Add pdoc3-generated API documentation.
authorBenjamin Braatz <bb@bbraatz.eu>
Sun, 21 Mar 2021 20:28:25 +0000 (21:28 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Sun, 21 Mar 2021 20:28:25 +0000 (21:28 +0100)
12 files changed:
controlpi/__init__.py
controlpi/baseplugin.py
controlpi_plugins/state.py
controlpi_plugins/util.py
doc/controlpi/baseplugin.html [new file with mode: 0644]
doc/controlpi/index.html [new file with mode: 0644]
doc/controlpi/messagebus.html [new file with mode: 0644]
doc/controlpi/pluginregistry.html [new file with mode: 0644]
doc/controlpi_plugins/index.html [new file with mode: 0644]
doc/controlpi_plugins/state.html [new file with mode: 0644]
doc/controlpi_plugins/util.html [new file with mode: 0644]
doc/controlpi_plugins/wait.html [new file with mode: 0644]

index 227672b384117e183b1ae6c174ac07232fed744f..fae63ec44d0712743c8f247c46797b99ec32759c 100644 (file)
@@ -133,7 +133,7 @@ async def test(conf: Dict[str, PluginConf],
 
     Similar functionality could be reached by using the Log and Init plugins
     to print messages and send some messages on the bus, but these would
-    clutter the test configuration and code to stop the idefinitely running
+    clutter the test configuration and code to stop the indefinitely running
     bus would have to be added to each and every test.
 
     Incorrect plugin configurations can also be tested by this:
index fa0322389f69983de73259817dac93836baaa508..589e1c94e6f26a26668d781b549ae16b26ccdd26 100644 (file)
@@ -79,6 +79,8 @@ might register separate clients for all devices connected to the bus, or a
 network socket plugin might register separate clients for all connections
 to the socket (and unregister them when the connection is closed).
 """
+__pdoc__ = {'BasePlugin.CONF_SCHEMA': False}
+
 from abc import ABC, abstractmethod
 import asyncio
 import jsonschema  # type: ignore
@@ -108,6 +110,50 @@ class BasePlugin(ABC):
     ...             print(f"Processing '{self.conf['key']}'.")
     ...     async def run(self):
     ...         print("Doing something else.")
+
+    Initialisation sets the instance variables bus to the given message bus,
+    name to the given name, and conf to the given configuration:
+    >>> class TestPlugin(BasePlugin):
+    ...     CONF_SCHEMA = {'properties': {'key': {'type': 'string'}},
+    ...                    'required': ['key']}
+    ...     def process_conf(self):
+    ...         if 'key' in self.conf:
+    ...             print(f"Processing '{self.conf['key']}'.")
+    ...     async def run(self):
+    ...         print("Doing something else.")
+    >>> async def test():
+    ...     p = TestPlugin(MessageBus(), 'Test Instance',
+    ...                    {'key': 'Something'})
+    ...     print(p.bus)
+    ...     print(p.name)
+    ...     print(p.conf)
+    >>> asyncio.run(test())  # doctest: +ELLIPSIS
+    Processing 'Something'.
+    <controlpi.messagebus.MessageBus object at 0x...>
+    Test Instance
+    {'key': 'Something'}
+
+    It also validates the configuration against the schema in CONF_SCHEMA
+    and raises ConfException if is not validated.
+    >>> async def test():
+    ...     p = TestPlugin(MessageBus(), 'Test Instance',
+    ...                    {'key': 42})
+    >>> asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for 'Test Instance'
+    is not valid.
+    >>> async def test():
+    ...     p = TestPlugin(MessageBus(), 'Test Instance',
+    ...                    {'key 2': 'Something'})
+    >>> asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for 'Test Instance'
+    is not valid.
+
+    Finally, it calls process_conf, which is the function that should be
+    overridden by concrete plugins.
     """
 
     @property
@@ -122,52 +168,7 @@ class BasePlugin(ABC):
         raise NotImplementedError
 
     def __init__(self, bus: MessageBus, name: str, conf: PluginConf) -> None:
-        """Initialise the plugin.
-
-        Set the instance variables bus to the given message bus, name to
-        the given name, and conf to the given configuration:
-        >>> class TestPlugin(BasePlugin):
-        ...     CONF_SCHEMA = {'properties': {'key': {'type': 'string'}},
-        ...                    'required': ['key']}
-        ...     def process_conf(self):
-        ...         if 'key' in self.conf:
-        ...             print(f"Processing '{self.conf['key']}'.")
-        ...     async def run(self):
-        ...         print("Doing something else.")
-        >>> async def test():
-        ...     p = TestPlugin(MessageBus(), 'Test Instance',
-        ...                    {'key': 'Something'})
-        ...     print(p.bus)
-        ...     print(p.name)
-        ...     print(p.conf)
-        >>> asyncio.run(test())  # doctest: +ELLIPSIS
-        Processing 'Something'.
-        <controlpi.messagebus.MessageBus object at 0x...>
-        Test Instance
-        {'key': 'Something'}
-
-        Validate the configuration against the schema in CONF_SCHEMA and
-        raise ConfException if is not validated.
-        >>> async def test():
-        ...     p = TestPlugin(MessageBus(), 'Test Instance',
-        ...                    {'key': 42})
-        >>> asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
-        Traceback (most recent call last):
-          ...
-        baseplugin.ConfException: Configuration for 'Test Instance'
-        is not valid.
-        >>> async def test():
-        ...     p = TestPlugin(MessageBus(), 'Test Instance',
-        ...                    {'key 2': 'Something'})
-        >>> asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
-        Traceback (most recent call last):
-          ...
-        baseplugin.ConfException: Configuration for 'Test Instance'
-        is not valid.
-
-        Finally, call process_conf, which is the function that should be
-        overridden by concrete plugins.
-        """
+        # noqa: D107
         assert isinstance(bus, MessageBus)
         self.bus = bus
         assert isinstance(name, str)
index a60233dc4da9679da96de6759a21a3850e92f1f3..ae4ab5978bf056b61b27713d07ea92783c788b2d 100644 (file)
@@ -8,14 +8,14 @@
 All these plugins use the following conventions:
 
 - If their state changes they send a message containing "event": "changed"
-  and "state": <new state>.
+  and "state": NEW STATE.
 - If their state is reported due to a message, but did not change they send
-  a message containing just "state": <current state>.
-- If they receive a message containing "target": <name> and
+  a message containing just "state": CURRENT STATE.
+- If they receive a message containing "target": NAME and
   "command": "get state" they report their current state.
 - If State (or any other settable state using these conventions) receives
-  a message containing "target": <name>, "command": "set state" and
-  "new state": <state to be set> it changes the state accordingly. If this
+  a message containing "target": NAME, "command": "set state" and
+  "new state": STATE TO SET it changes the state accordingly. If this
   was really a change the corresponding event is sent. If it was already in
   this state a report message without "event": "changed" is sent.
 - StateAlias can alias any message bus client using these conventions, not
index 313e50b8a987c226e1899edb198e511623d322eb..cf6e86926f10dd876ad306706f4dbebaf9dda17b 100644 (file)
@@ -2,6 +2,7 @@
 
 - Log logs messages on stdout.
 - Init sends list of messages on startup and on demand.
+- Execute sends configurable list of messages on demand.
 - Alias translates messages to an alias.
 
 >>> import controlpi
@@ -125,7 +126,7 @@ class Init(BasePlugin):
 
     The "messages" configuration key gets a list of messages to be sent on
     startup. The same list is sent in reaction to a message with
-    "target": <name> and "command": "execute".
+    "target": NAME and "command": "execute".
 
     In the example, the two configured messages are sent twice, once at
     startup and a second time in reaction to the "execute" command sent by
diff --git a/doc/controlpi/baseplugin.html b/doc/controlpi/baseplugin.html
new file mode 100644 (file)
index 0000000..7f2f42f
--- /dev/null
@@ -0,0 +1,621 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi.baseplugin API documentation</title>
+<meta name="description" content="Define base class for all ControlPi plugins …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi.baseplugin</code></h1>
+</header>
+<section id="section-intro">
+<p>Define base class for all ControlPi plugins.</p>
+<p>The class BasePlugin provides the abstract base class for concrete plugins
+running on the ControlPi system.</p>
+<p>It has three abstract methods that have to be implemented by all concrete
+plugins:
+- The class property CONF_SCHEMA is the JSON schema of the configuration of
+the plugin. The configuration read from the global configuration file is
+checked against this schema during initialisation.
+- The method process_conf is called at the end of initialisation and is used
+to initialise the plugin. It can be assumed that self.bus is the message
+bus of the system, self.name the instance name, and self.conf the
+configuration already validated against the schema.
+- The run coroutines of all plugins are executed concurrently by the main
+system.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class TestPlugin(BasePlugin):
+...     CONF_SCHEMA = {'properties': {'key': {'type': 'string'}},
+...                    'required': ['key']}
+...     def process_conf(self):
+...         if 'key' in self.conf:
+...             print(f&quot;Processing '{self.conf['key']}'.&quot;)
+...     async def run(self):
+...         print(&quot;Doing something else.&quot;)
+</code></pre>
+<p>Plugins are configured and run based on the information in the global
+configuration. Here, we test this manually:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def test():
+...     p = TestPlugin(MessageBus(), 'Test Instance', {'key': 'Something'})
+...     await p.run()
+&gt;&gt;&gt; asyncio.run(test())
+Processing 'Something'.
+Doing something else.
+</code></pre>
+<p>Each plugin gets a reference to the system message bus during
+initialisation, which can be accessed as self.bus in the functions of the
+plugin class. This can be used to register and unregister message bus
+clients:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class BusPlugin(BasePlugin):
+...     CONF_SCHEMA = True
+...     async def receive(self, message):
+...         print(f&quot;{self.name} received {message}.&quot;)
+...         await self.bus.send({'sender': self.name, 'event': 'Receive'})
+...     def process_conf(self):
+...         self.bus.register(self.name, 'BusPlugin',
+...                           [{'event': {'type': 'string'}}],
+...                           [{'target': {'const': self.name}}],
+...                           self.receive)
+...     async def run(self):
+...         await self.bus.send({'sender': self.name, 'event': 'Run'})
+</code></pre>
+<p>Again, we run this manually here, but this is done by the main coroutine
+when using the system in production:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def log(message):
+...     print(f&quot;Log: {message}&quot;)
+&gt;&gt;&gt; async def test_bus_plugin():
+...     bus = MessageBus()
+...     p = BusPlugin(bus, 'Bus Test', {})
+...     bus.register('Test', 'TestPlugin',
+...                  [{}], [{'sender': {'const': 'Bus Test'}}], log)
+...     bus_task = asyncio.create_task(bus.run())
+...     asyncio.create_task(p.run())
+...     await bus.send({'sender': 'Test', 'target': 'Bus Test', 'key': 'v'})
+...     await asyncio.sleep(0)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(test_bus_plugin())
+Bus Test received {'sender': 'Test', 'target': 'Bus Test', 'key': 'v'}.
+Log: {'sender': 'Bus Test', 'event': 'Receive'}
+Log: {'sender': 'Bus Test', 'event': 'Run'}
+</code></pre>
+<p>Often, there will be a one-to-one correspondence between plugin
+instances and message bus clients, a plugin instance will be a message bus
+client. But there are also cases, where one plugin instance might register
+and unregister a lot of message bus clients, maybe even dynamically through
+its lifetime. A plugin for an input/output card might register separate
+clients for each pin of the card, a plugin for some kind of hardware bus
+might register separate clients for all devices connected to the bus, or a
+network socket plugin might register separate clients for all connections
+to the socket (and unregister them when the connection is closed).</p>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Define base class for all ControlPi plugins.
+
+The class BasePlugin provides the abstract base class for concrete plugins
+running on the ControlPi system.
+
+It has three abstract methods that have to be implemented by all concrete
+plugins:
+- The class property CONF_SCHEMA is the JSON schema of the configuration of
+  the plugin. The configuration read from the global configuration file is
+  checked against this schema during initialisation.
+- The method process_conf is called at the end of initialisation and is used
+  to initialise the plugin. It can be assumed that self.bus is the message
+  bus of the system, self.name the instance name, and self.conf the
+  configuration already validated against the schema.
+- The run coroutines of all plugins are executed concurrently by the main
+  system.
+&gt;&gt;&gt; class TestPlugin(BasePlugin):
+...     CONF_SCHEMA = {&#39;properties&#39;: {&#39;key&#39;: {&#39;type&#39;: &#39;string&#39;}},
+...                    &#39;required&#39;: [&#39;key&#39;]}
+...     def process_conf(self):
+...         if &#39;key&#39; in self.conf:
+...             print(f&#34;Processing &#39;{self.conf[&#39;key&#39;]}&#39;.&#34;)
+...     async def run(self):
+...         print(&#34;Doing something else.&#34;)
+
+Plugins are configured and run based on the information in the global
+configuration. Here, we test this manually:
+&gt;&gt;&gt; async def test():
+...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;, {&#39;key&#39;: &#39;Something&#39;})
+...     await p.run()
+&gt;&gt;&gt; asyncio.run(test())
+Processing &#39;Something&#39;.
+Doing something else.
+
+Each plugin gets a reference to the system message bus during
+initialisation, which can be accessed as self.bus in the functions of the
+plugin class. This can be used to register and unregister message bus
+clients:
+&gt;&gt;&gt; class BusPlugin(BasePlugin):
+...     CONF_SCHEMA = True
+...     async def receive(self, message):
+...         print(f&#34;{self.name} received {message}.&#34;)
+...         await self.bus.send({&#39;sender&#39;: self.name, &#39;event&#39;: &#39;Receive&#39;})
+...     def process_conf(self):
+...         self.bus.register(self.name, &#39;BusPlugin&#39;,
+...                           [{&#39;event&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+...                           [{&#39;target&#39;: {&#39;const&#39;: self.name}}],
+...                           self.receive)
+...     async def run(self):
+...         await self.bus.send({&#39;sender&#39;: self.name, &#39;event&#39;: &#39;Run&#39;})
+
+Again, we run this manually here, but this is done by the main coroutine
+when using the system in production:
+&gt;&gt;&gt; async def log(message):
+...     print(f&#34;Log: {message}&#34;)
+&gt;&gt;&gt; async def test_bus_plugin():
+...     bus = MessageBus()
+...     p = BusPlugin(bus, &#39;Bus Test&#39;, {})
+...     bus.register(&#39;Test&#39;, &#39;TestPlugin&#39;,
+...                  [{}], [{&#39;sender&#39;: {&#39;const&#39;: &#39;Bus Test&#39;}}], log)
+...     bus_task = asyncio.create_task(bus.run())
+...     asyncio.create_task(p.run())
+...     await bus.send({&#39;sender&#39;: &#39;Test&#39;, &#39;target&#39;: &#39;Bus Test&#39;, &#39;key&#39;: &#39;v&#39;})
+...     await asyncio.sleep(0)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(test_bus_plugin())
+Bus Test received {&#39;sender&#39;: &#39;Test&#39;, &#39;target&#39;: &#39;Bus Test&#39;, &#39;key&#39;: &#39;v&#39;}.
+Log: {&#39;sender&#39;: &#39;Bus Test&#39;, &#39;event&#39;: &#39;Receive&#39;}
+Log: {&#39;sender&#39;: &#39;Bus Test&#39;, &#39;event&#39;: &#39;Run&#39;}
+
+Often, there will be a one-to-one correspondence between plugin
+instances and message bus clients, a plugin instance will be a message bus
+client. But there are also cases, where one plugin instance might register
+and unregister a lot of message bus clients, maybe even dynamically through
+its lifetime. A plugin for an input/output card might register separate
+clients for each pin of the card, a plugin for some kind of hardware bus
+might register separate clients for all devices connected to the bus, or a
+network socket plugin might register separate clients for all connections
+to the socket (and unregister them when the connection is closed).
+&#34;&#34;&#34;
+__pdoc__ = {&#39;BasePlugin.CONF_SCHEMA&#39;: False}
+
+from abc import ABC, abstractmethod
+import asyncio
+import jsonschema  # type: ignore
+
+from controlpi.messagebus import MessageBus
+
+from typing import Union, Dict, List, Any
+JSONSchema = Union[bool, Dict[str, Union[None, str, int, float, bool,
+                                         Dict[str, Any], List[Any]]]]
+# Could be more specific.
+PluginConf = Dict[str, Any]
+# Could be more specific.
+
+
+class ConfException(Exception):
+    &#34;&#34;&#34;Raise for errors in plugin configurations.&#34;&#34;&#34;
+
+
+class BasePlugin(ABC):
+    &#34;&#34;&#34;Base class for all ControlPi plugins.
+
+    &gt;&gt;&gt; class TestPlugin(BasePlugin):
+    ...     CONF_SCHEMA = {&#39;properties&#39;: {&#39;key&#39;: {&#39;type&#39;: &#39;string&#39;}},
+    ...                    &#39;required&#39;: [&#39;key&#39;]}
+    ...     def process_conf(self):
+    ...         if &#39;key&#39; in self.conf:
+    ...             print(f&#34;Processing &#39;{self.conf[&#39;key&#39;]}&#39;.&#34;)
+    ...     async def run(self):
+    ...         print(&#34;Doing something else.&#34;)
+
+    Initialisation sets the instance variables bus to the given message bus,
+    name to the given name, and conf to the given configuration:
+    &gt;&gt;&gt; class TestPlugin(BasePlugin):
+    ...     CONF_SCHEMA = {&#39;properties&#39;: {&#39;key&#39;: {&#39;type&#39;: &#39;string&#39;}},
+    ...                    &#39;required&#39;: [&#39;key&#39;]}
+    ...     def process_conf(self):
+    ...         if &#39;key&#39; in self.conf:
+    ...             print(f&#34;Processing &#39;{self.conf[&#39;key&#39;]}&#39;.&#34;)
+    ...     async def run(self):
+    ...         print(&#34;Doing something else.&#34;)
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key&#39;: &#39;Something&#39;})
+    ...     print(p.bus)
+    ...     print(p.name)
+    ...     print(p.conf)
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +ELLIPSIS
+    Processing &#39;Something&#39;.
+    &lt;controlpi.messagebus.MessageBus object at 0x...&gt;
+    Test Instance
+    {&#39;key&#39;: &#39;Something&#39;}
+
+    It also validates the configuration against the schema in CONF_SCHEMA
+    and raises ConfException if is not validated.
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key&#39;: 42})
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for &#39;Test Instance&#39;
+    is not valid.
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key 2&#39;: &#39;Something&#39;})
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for &#39;Test Instance&#39;
+    is not valid.
+
+    Finally, it calls process_conf, which is the function that should be
+    overridden by concrete plugins.
+    &#34;&#34;&#34;
+
+    @property
+    @classmethod
+    @abstractmethod
+    def CONF_SCHEMA(cls) -&gt; JSONSchema:
+        &#34;&#34;&#34;JSON schema for configuration of plugin.
+
+        Given configurations are validated against this schema in __init__.
+        process_conf and run can assume a valid configuration in self.conf.
+        &#34;&#34;&#34;
+        raise NotImplementedError
+
+    def __init__(self, bus: MessageBus, name: str, conf: PluginConf) -&gt; None:
+        assert isinstance(bus, MessageBus)
+        self.bus = bus
+        assert isinstance(name, str)
+        self.name = name
+        jsonschema.Draft7Validator.check_schema(type(self).CONF_SCHEMA)
+        validator = jsonschema.Draft7Validator(type(self).CONF_SCHEMA)
+        assert isinstance(conf, dict)
+        valid = True
+        for error in validator.iter_errors(conf):
+            print(error)
+            valid = False
+        if not valid:
+            raise ConfException(f&#34;Configuration for &#39;{self.name}&#39;&#34;
+                                &#34; is not valid.&#34;)
+        self.conf = conf
+        self.process_conf()
+
+    @abstractmethod
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Process the configuration.
+
+        Abstract method has to be overridden by concrete plugins.
+        process_conf is called at the end of initialisation after the bus
+        and the configuration are available as self.bus and self.conf, but
+        before any of the run coroutines are executed.
+        &#34;&#34;&#34;
+        raise NotImplementedError
+
+    @abstractmethod
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run the plugin.
+
+        The coroutine is run concurrently with the message bus and all
+        other plugins. Initial messages and other tasks can be done here.
+        It is also okay to run a plugin-specific infinite loop concurrently
+        with the rest of the system.
+        &#34;&#34;&#34;
+        raise NotImplementedError</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi.baseplugin.ConfException"><code class="flex name class">
+<span>class <span class="ident">ConfException</span></span>
+<span>(</span><span>*args, **kwargs)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Raise for errors in plugin configurations.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class ConfException(Exception):
+    &#34;&#34;&#34;Raise for errors in plugin configurations.&#34;&#34;&#34;</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>builtins.Exception</li>
+<li>builtins.BaseException</li>
+</ul>
+</dd>
+<dt id="controlpi.baseplugin.BasePlugin"><code class="flex name class">
+<span>class <span class="ident">BasePlugin</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Base class for all ControlPi plugins.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class TestPlugin(BasePlugin):
+...     CONF_SCHEMA = {'properties': {'key': {'type': 'string'}},
+...                    'required': ['key']}
+...     def process_conf(self):
+...         if 'key' in self.conf:
+...             print(f&quot;Processing '{self.conf['key']}'.&quot;)
+...     async def run(self):
+...         print(&quot;Doing something else.&quot;)
+</code></pre>
+<p>Initialisation sets the instance variables bus to the given message bus,
+name to the given name, and conf to the given configuration:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class TestPlugin(BasePlugin):
+...     CONF_SCHEMA = {'properties': {'key': {'type': 'string'}},
+...                    'required': ['key']}
+...     def process_conf(self):
+...         if 'key' in self.conf:
+...             print(f&quot;Processing '{self.conf['key']}'.&quot;)
+...     async def run(self):
+...         print(&quot;Doing something else.&quot;)
+&gt;&gt;&gt; async def test():
+...     p = TestPlugin(MessageBus(), 'Test Instance',
+...                    {'key': 'Something'})
+...     print(p.bus)
+...     print(p.name)
+...     print(p.conf)
+&gt;&gt;&gt; asyncio.run(test())  # doctest: +ELLIPSIS
+Processing 'Something'.
+&lt;controlpi.messagebus.MessageBus object at 0x...&gt;
+Test Instance
+{'key': 'Something'}
+</code></pre>
+<p>It also validates the configuration against the schema in CONF_SCHEMA
+and raises ConfException if is not validated.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def test():
+...     p = TestPlugin(MessageBus(), 'Test Instance',
+...                    {'key': 42})
+&gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+Traceback (most recent call last):
+  ...
+baseplugin.ConfException: Configuration for 'Test Instance'
+is not valid.
+&gt;&gt;&gt; async def test():
+...     p = TestPlugin(MessageBus(), 'Test Instance',
+...                    {'key 2': 'Something'})
+&gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+Traceback (most recent call last):
+  ...
+baseplugin.ConfException: Configuration for 'Test Instance'
+is not valid.
+</code></pre>
+<p>Finally, it calls process_conf, which is the function that should be
+overridden by concrete plugins.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class BasePlugin(ABC):
+    &#34;&#34;&#34;Base class for all ControlPi plugins.
+
+    &gt;&gt;&gt; class TestPlugin(BasePlugin):
+    ...     CONF_SCHEMA = {&#39;properties&#39;: {&#39;key&#39;: {&#39;type&#39;: &#39;string&#39;}},
+    ...                    &#39;required&#39;: [&#39;key&#39;]}
+    ...     def process_conf(self):
+    ...         if &#39;key&#39; in self.conf:
+    ...             print(f&#34;Processing &#39;{self.conf[&#39;key&#39;]}&#39;.&#34;)
+    ...     async def run(self):
+    ...         print(&#34;Doing something else.&#34;)
+
+    Initialisation sets the instance variables bus to the given message bus,
+    name to the given name, and conf to the given configuration:
+    &gt;&gt;&gt; class TestPlugin(BasePlugin):
+    ...     CONF_SCHEMA = {&#39;properties&#39;: {&#39;key&#39;: {&#39;type&#39;: &#39;string&#39;}},
+    ...                    &#39;required&#39;: [&#39;key&#39;]}
+    ...     def process_conf(self):
+    ...         if &#39;key&#39; in self.conf:
+    ...             print(f&#34;Processing &#39;{self.conf[&#39;key&#39;]}&#39;.&#34;)
+    ...     async def run(self):
+    ...         print(&#34;Doing something else.&#34;)
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key&#39;: &#39;Something&#39;})
+    ...     print(p.bus)
+    ...     print(p.name)
+    ...     print(p.conf)
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +ELLIPSIS
+    Processing &#39;Something&#39;.
+    &lt;controlpi.messagebus.MessageBus object at 0x...&gt;
+    Test Instance
+    {&#39;key&#39;: &#39;Something&#39;}
+
+    It also validates the configuration against the schema in CONF_SCHEMA
+    and raises ConfException if is not validated.
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key&#39;: 42})
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for &#39;Test Instance&#39;
+    is not valid.
+    &gt;&gt;&gt; async def test():
+    ...     p = TestPlugin(MessageBus(), &#39;Test Instance&#39;,
+    ...                    {&#39;key 2&#39;: &#39;Something&#39;})
+    &gt;&gt;&gt; asyncio.run(test())  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    baseplugin.ConfException: Configuration for &#39;Test Instance&#39;
+    is not valid.
+
+    Finally, it calls process_conf, which is the function that should be
+    overridden by concrete plugins.
+    &#34;&#34;&#34;
+
+    @property
+    @classmethod
+    @abstractmethod
+    def CONF_SCHEMA(cls) -&gt; JSONSchema:
+        &#34;&#34;&#34;JSON schema for configuration of plugin.
+
+        Given configurations are validated against this schema in __init__.
+        process_conf and run can assume a valid configuration in self.conf.
+        &#34;&#34;&#34;
+        raise NotImplementedError
+
+    def __init__(self, bus: MessageBus, name: str, conf: PluginConf) -&gt; None:
+        assert isinstance(bus, MessageBus)
+        self.bus = bus
+        assert isinstance(name, str)
+        self.name = name
+        jsonschema.Draft7Validator.check_schema(type(self).CONF_SCHEMA)
+        validator = jsonschema.Draft7Validator(type(self).CONF_SCHEMA)
+        assert isinstance(conf, dict)
+        valid = True
+        for error in validator.iter_errors(conf):
+            print(error)
+            valid = False
+        if not valid:
+            raise ConfException(f&#34;Configuration for &#39;{self.name}&#39;&#34;
+                                &#34; is not valid.&#34;)
+        self.conf = conf
+        self.process_conf()
+
+    @abstractmethod
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Process the configuration.
+
+        Abstract method has to be overridden by concrete plugins.
+        process_conf is called at the end of initialisation after the bus
+        and the configuration are available as self.bus and self.conf, but
+        before any of the run coroutines are executed.
+        &#34;&#34;&#34;
+        raise NotImplementedError
+
+    @abstractmethod
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run the plugin.
+
+        The coroutine is run concurrently with the message bus and all
+        other plugins. Initial messages and other tasks can be done here.
+        It is also okay to run a plugin-specific infinite loop concurrently
+        with the rest of the system.
+        &#34;&#34;&#34;
+        raise NotImplementedError</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>abc.ABC</li>
+</ul>
+<h3>Subclasses</h3>
+<ul class="hlist">
+<li><a title="controlpi_plugins.state.AndState" href="../controlpi_plugins/state.html#controlpi_plugins.state.AndState">AndState</a></li>
+<li><a title="controlpi_plugins.state.OrState" href="../controlpi_plugins/state.html#controlpi_plugins.state.OrState">OrState</a></li>
+<li><a title="controlpi_plugins.state.State" href="../controlpi_plugins/state.html#controlpi_plugins.state.State">State</a></li>
+<li><a title="controlpi_plugins.state.StateAlias" href="../controlpi_plugins/state.html#controlpi_plugins.state.StateAlias">StateAlias</a></li>
+<li><a title="controlpi_plugins.util.Alias" href="../controlpi_plugins/util.html#controlpi_plugins.util.Alias">Alias</a></li>
+<li><a title="controlpi_plugins.util.Execute" href="../controlpi_plugins/util.html#controlpi_plugins.util.Execute">Execute</a></li>
+<li><a title="controlpi_plugins.util.Init" href="../controlpi_plugins/util.html#controlpi_plugins.util.Init">Init</a></li>
+<li><a title="controlpi_plugins.util.Log" href="../controlpi_plugins/util.html#controlpi_plugins.util.Log">Log</a></li>
+<li><a title="controlpi_plugins.wait.GenericWait" href="../controlpi_plugins/wait.html#controlpi_plugins.wait.GenericWait">GenericWait</a></li>
+<li><a title="controlpi_plugins.wait.Wait" href="../controlpi_plugins/wait.html#controlpi_plugins.wait.Wait">Wait</a></li>
+</ul>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi.baseplugin.BasePlugin.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Process the configuration.</p>
+<p>Abstract method has to be overridden by concrete plugins.
+process_conf is called at the end of initialisation after the bus
+and the configuration are available as self.bus and self.conf, but
+before any of the run coroutines are executed.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">@abstractmethod
+def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Process the configuration.
+
+    Abstract method has to be overridden by concrete plugins.
+    process_conf is called at the end of initialisation after the bus
+    and the configuration are available as self.bus and self.conf, but
+    before any of the run coroutines are executed.
+    &#34;&#34;&#34;
+    raise NotImplementedError</code></pre>
+</details>
+</dd>
+<dt id="controlpi.baseplugin.BasePlugin.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run the plugin.</p>
+<p>The coroutine is run concurrently with the message bus and all
+other plugins. Initial messages and other tasks can be done here.
+It is also okay to run a plugin-specific infinite loop concurrently
+with the rest of the system.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">@abstractmethod
+async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run the plugin.
+
+    The coroutine is run concurrently with the message bus and all
+    other plugins. Initial messages and other tasks can be done here.
+    It is also okay to run a plugin-specific infinite loop concurrently
+    with the rest of the system.
+    &#34;&#34;&#34;
+    raise NotImplementedError</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi" href="index.html">controlpi</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi.baseplugin.ConfException" href="#controlpi.baseplugin.ConfException">ConfException</a></code></h4>
+</li>
+<li>
+<h4><code><a title="controlpi.baseplugin.BasePlugin" href="#controlpi.baseplugin.BasePlugin">BasePlugin</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi.baseplugin.BasePlugin.process_conf" href="#controlpi.baseplugin.BasePlugin.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi.baseplugin.BasePlugin.run" href="#controlpi.baseplugin.BasePlugin.run">run</a></code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi/index.html b/doc/controlpi/index.html
new file mode 100644 (file)
index 0000000..d42fb6c
--- /dev/null
@@ -0,0 +1,476 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi API documentation</title>
+<meta name="description" content="Provide the infrastructure for the ControlPi system …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Package <code>controlpi</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide the infrastructure for the ControlPi system.</p>
+<p>The infrastructure consists of the message bus from module messagebus, the
+plugin registry from module pluginregistry and the abstract base plugin from
+module baseplugin.</p>
+<p>The package combines them in its run function, which is used by <strong>main</strong>.py
+to run a ControlPi system based on a configuration file indefinitely.</p>
+<p>The test function is a utility function to test plugins with minimal
+boilerplate code.</p>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide the infrastructure for the ControlPi system.
+
+The infrastructure consists of the message bus from module messagebus, the
+plugin registry from module pluginregistry and the abstract base plugin from
+module baseplugin.
+
+The package combines them in its run function, which is used by __main__.py
+to run a ControlPi system based on a configuration file indefinitely.
+
+The test function is a utility function to test plugins with minimal
+boilerplate code.
+&#34;&#34;&#34;
+import asyncio
+import jsonschema  # type: ignore
+
+from controlpi.messagebus import MessageBus, Message, MessageTemplate
+from controlpi.pluginregistry import PluginRegistry
+from controlpi.baseplugin import BasePlugin, PluginConf, ConfException
+
+from typing import Dict, List, Coroutine, Any
+
+
+CONF_SCHEMA = {&#39;type&#39;: &#39;object&#39;,
+               &#39;patternProperties&#39;: {&#39;.*&#39;: {&#39;type&#39;: &#39;object&#39;}}}
+
+
+def _process_conf(message_bus: MessageBus,
+                  conf: Dict[str, PluginConf]) -&gt; List[Coroutine]:
+    jsonschema.Draft7Validator.check_schema(CONF_SCHEMA)
+    validator = jsonschema.Draft7Validator(CONF_SCHEMA)
+    valid = True
+    for error in validator.iter_errors(conf):
+        print(error)
+        valid = False
+    if not valid:
+        return []
+    plugins = PluginRegistry(&#39;controlpi_plugins&#39;, BasePlugin)
+    coroutines = [message_bus.run()]
+    for instance_name in conf:
+        instance_conf = conf[instance_name]
+        if &#39;plugin&#39; not in instance_conf:
+            print(&#34;No plugin implementation specified for instance&#34;
+                  f&#34; &#39;{instance_name}&#39;.&#34;)
+            continue
+        plugin_name = instance_conf[&#39;plugin&#39;]
+        if plugin_name not in plugins:
+            print(f&#34;No implementation found for plugin &#39;{plugin_name}&#39;&#34;
+                  f&#34; (specified for instance &#39;{instance_name}&#39;).&#34;)
+            continue
+        plugin = plugins[plugin_name]
+        try:
+            instance = plugin(message_bus, instance_name, instance_conf)
+            coroutines.append(instance.run())
+        except ConfException as e:
+            print(e)
+            continue
+    return coroutines
+
+
+async def run(conf: Dict[str, PluginConf]) -&gt; None:
+    &#34;&#34;&#34;Run the ControlPi system based on a configuration.
+
+    Setup message bus, process given configuration, and run message bus and
+    plugins concurrently and indefinitely.
+
+    This function is mainly used by __main__.py to run a ControlPi system
+    based on a configuration loaded from a configuration JSON file on disk.
+
+    &gt;&gt;&gt; async def test_coroutine():
+    ...     conf = {&#34;Example Init&#34;:
+    ...             {&#34;plugin&#34;: &#34;Init&#34;,
+    ...              &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                            &#34;content&#34;: &#34;Test Message&#34;},
+    ...                           {&#34;id&#34;: 42.42,
+    ...                            &#34;content&#34;: &#34;Second Message&#34;}]},
+    ...             &#34;Example Log&#34;:
+    ...             {&#34;plugin&#34;: &#34;Log&#34;,
+    ...              &#34;filter&#34;: [{&#34;sender&#34;: {&#34;const&#34;: &#34;Example Init&#34;}}]}}
+    ...     run_task = asyncio.create_task(run(conf))
+    ...     await asyncio.sleep(0.1)
+    ...     run_task.cancel()
+    &gt;&gt;&gt; asyncio.run(test_coroutine())  # doctest: +NORMALIZE_WHITESPACE
+    Example Log: {&#39;sender&#39;: &#39;Example Init&#39;,
+                  &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    Example Log: {&#39;sender&#39;: &#39;Example Init&#39;,
+                  &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    &#34;&#34;&#34;
+    message_bus = MessageBus()
+    coroutines = _process_conf(message_bus, conf)
+    try:
+        await asyncio.gather(*coroutines)
+    except asyncio.exceptions.CancelledError:
+        pass
+
+
+async def test(conf: Dict[str, PluginConf],
+               messages: List[Dict[str, Any]],
+               wait: float = 0.0) -&gt; None:
+    &#34;&#34;&#34;Test configuration of ControlPi system.
+
+    Setup message bus, process given configuration, run message bus and
+    plugins concurrently, send given messages on message bus and print all
+    messages on message bus. Terminate when queue of message bus is empty.
+
+    This function allows to test single plugins or small plugin
+    configurations with minimal boilerplate code:
+    &gt;&gt;&gt; asyncio.run(test(
+    ...     {&#34;Example Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                       &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                                     &#34;content&#34;: &#34;Test Message&#34;},
+    ...                                    {&#34;id&#34;: 42.42,
+    ...                                     &#34;content&#34;: &#34;Second Message&#34;}]}},
+    ...     [{&#34;target&#34;: &#34;Example Init&#34;,
+    ...       &#34;command&#34;: &#34;execute&#34;}]))  # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Example Init&#39;, &#39;plugin&#39;: &#39;Init&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Test Message&#39;}},
+                       {&#39;id&#39;: {&#39;const&#39;: 42.42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Second Message&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Example Init&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Example Init&#39;,
+             &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+
+    Similar functionality could be reached by using the Log and Init plugins
+    to print messages and send some messages on the bus, but these would
+    clutter the test configuration and code to stop the indefinitely running
+    bus would have to be added to each and every test.
+
+    Incorrect plugin configurations can also be tested by this:
+    &gt;&gt;&gt; asyncio.run(test(
+    ...     {&#34;Example Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;}}, []))
+    &#39;messages&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;messages&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                     &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;messages&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Init&#39;}
+    Configuration for &#39;Example Init&#39; is not valid.
+    &#34;&#34;&#34;
+    message_bus = MessageBus()
+
+    async def log(message):
+        if (&#39;sender&#39; in message and message[&#39;sender&#39;] == &#39;&#39; and
+                &#39;event&#39; in message and message[&#39;event&#39;] == &#39;registered&#39; and
+                &#39;client&#39; in message and message[&#39;client&#39;] == &#39;test()&#39;):
+            # Do not log own registration of &#39;test()&#39;:
+            return
+        print(f&#34;test(): {message}&#34;)
+    message_bus.register(&#39;test()&#39;, &#39;Test&#39;,
+                         [MessageTemplate()], [MessageTemplate()], log)
+
+    coroutines = _process_conf(message_bus, conf)
+    for coroutine in coroutines:
+        asyncio.create_task(coroutine)
+        # Give the created task opportunity to run:
+        await asyncio.sleep(0)
+    for message in messages:
+        await message_bus.send(Message(&#39;test()&#39;, message))
+        # Give immediate reactions to messages opportunity to happen:
+        await asyncio.sleep(0)
+    await asyncio.sleep(wait)
+    await message_bus._queue.join()</code></pre>
+</details>
+</section>
+<section>
+<h2 class="section-title" id="header-submodules">Sub-modules</h2>
+<dl>
+<dt><code class="name"><a title="controlpi.baseplugin" href="baseplugin.html">controlpi.baseplugin</a></code></dt>
+<dd>
+<div class="desc"><p>Define base class for all ControlPi plugins …</p></div>
+</dd>
+<dt><code class="name"><a title="controlpi.messagebus" href="messagebus.html">controlpi.messagebus</a></code></dt>
+<dd>
+<div class="desc"><p>Provide an asynchronous message bus …</p></div>
+</dd>
+<dt><code class="name"><a title="controlpi.pluginregistry" href="pluginregistry.html">controlpi.pluginregistry</a></code></dt>
+<dd>
+<div class="desc"><p>Provide a generic plugin system …</p></div>
+</dd>
+</dl>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-functions">Functions</h2>
+<dl>
+<dt id="controlpi.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>conf: Dict[str, Dict[str, Any]]) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run the ControlPi system based on a configuration.</p>
+<p>Setup message bus, process given configuration, and run message bus and
+plugins concurrently and indefinitely.</p>
+<p>This function is mainly used by <strong>main</strong>.py to run a ControlPi system
+based on a configuration loaded from a configuration JSON file on disk.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def test_coroutine():
+...     conf = {&quot;Example Init&quot;:
+...             {&quot;plugin&quot;: &quot;Init&quot;,
+...              &quot;messages&quot;: [{&quot;id&quot;: 42,
+...                            &quot;content&quot;: &quot;Test Message&quot;},
+...                           {&quot;id&quot;: 42.42,
+...                            &quot;content&quot;: &quot;Second Message&quot;}]},
+...             &quot;Example Log&quot;:
+...             {&quot;plugin&quot;: &quot;Log&quot;,
+...              &quot;filter&quot;: [{&quot;sender&quot;: {&quot;const&quot;: &quot;Example Init&quot;}}]}}
+...     run_task = asyncio.create_task(run(conf))
+...     await asyncio.sleep(0.1)
+...     run_task.cancel()
+&gt;&gt;&gt; asyncio.run(test_coroutine())  # doctest: +NORMALIZE_WHITESPACE
+Example Log: {'sender': 'Example Init',
+              'id': 42, 'content': 'Test Message'}
+Example Log: {'sender': 'Example Init',
+              'id': 42.42, 'content': 'Second Message'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(conf: Dict[str, PluginConf]) -&gt; None:
+    &#34;&#34;&#34;Run the ControlPi system based on a configuration.
+
+    Setup message bus, process given configuration, and run message bus and
+    plugins concurrently and indefinitely.
+
+    This function is mainly used by __main__.py to run a ControlPi system
+    based on a configuration loaded from a configuration JSON file on disk.
+
+    &gt;&gt;&gt; async def test_coroutine():
+    ...     conf = {&#34;Example Init&#34;:
+    ...             {&#34;plugin&#34;: &#34;Init&#34;,
+    ...              &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                            &#34;content&#34;: &#34;Test Message&#34;},
+    ...                           {&#34;id&#34;: 42.42,
+    ...                            &#34;content&#34;: &#34;Second Message&#34;}]},
+    ...             &#34;Example Log&#34;:
+    ...             {&#34;plugin&#34;: &#34;Log&#34;,
+    ...              &#34;filter&#34;: [{&#34;sender&#34;: {&#34;const&#34;: &#34;Example Init&#34;}}]}}
+    ...     run_task = asyncio.create_task(run(conf))
+    ...     await asyncio.sleep(0.1)
+    ...     run_task.cancel()
+    &gt;&gt;&gt; asyncio.run(test_coroutine())  # doctest: +NORMALIZE_WHITESPACE
+    Example Log: {&#39;sender&#39;: &#39;Example Init&#39;,
+                  &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    Example Log: {&#39;sender&#39;: &#39;Example Init&#39;,
+                  &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    &#34;&#34;&#34;
+    message_bus = MessageBus()
+    coroutines = _process_conf(message_bus, conf)
+    try:
+        await asyncio.gather(*coroutines)
+    except asyncio.exceptions.CancelledError:
+        pass</code></pre>
+</details>
+</dd>
+<dt id="controlpi.test"><code class="name flex">
+<span>async def <span class="ident">test</span></span>(<span>conf: Dict[str, Dict[str, Any]], messages: List[Dict[str, Any]], wait: float = 0.0) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Test configuration of ControlPi system.</p>
+<p>Setup message bus, process given configuration, run message bus and
+plugins concurrently, send given messages on message bus and print all
+messages on message bus. Terminate when queue of message bus is empty.</p>
+<p>This function allows to test single plugins or small plugin
+configurations with minimal boilerplate code:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(test(
+...     {&quot;Example Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;,
+...                       &quot;messages&quot;: [{&quot;id&quot;: 42,
+...                                     &quot;content&quot;: &quot;Test Message&quot;},
+...                                    {&quot;id&quot;: 42.42,
+...                                     &quot;content&quot;: &quot;Second Message&quot;}]}},
+...     [{&quot;target&quot;: &quot;Example Init&quot;,
+...       &quot;command&quot;: &quot;execute&quot;}]))  # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Example Init', 'plugin': 'Init',
+         'sends': [{'id': {'const': 42},
+                    'content': {'const': 'Test Message'}},
+                   {'id': {'const': 42.42},
+                    'content': {'const': 'Second Message'}}],
+         'receives': [{'target': {'const': 'Example Init'},
+                       'command': {'const': 'execute'}}]}
+test(): {'sender': 'Example Init',
+         'id': 42, 'content': 'Test Message'}
+test(): {'sender': 'Example Init',
+         'id': 42.42, 'content': 'Second Message'}
+test(): {'sender': 'test()', 'target': 'Example Init',
+         'command': 'execute'}
+test(): {'sender': 'Example Init',
+         'id': 42, 'content': 'Test Message'}
+test(): {'sender': 'Example Init',
+         'id': 42.42, 'content': 'Second Message'}
+</code></pre>
+<p>Similar functionality could be reached by using the Log and Init plugins
+to print messages and send some messages on the bus, but these would
+clutter the test configuration and code to stop the indefinitely running
+bus would have to be added to each and every test.</p>
+<p>Incorrect plugin configurations can also be tested by this:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(test(
+...     {&quot;Example Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;}}, []))
+'messages' is a required property
+&lt;BLANKLINE&gt;
+Failed validating 'required' in schema:
+    {'properties': {'messages': {'items': {'type': 'object'},
+                                 'type': 'array'}},
+     'required': ['messages']}
+&lt;BLANKLINE&gt;
+On instance:
+    {'plugin': 'Init'}
+Configuration for 'Example Init' is not valid.
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def test(conf: Dict[str, PluginConf],
+               messages: List[Dict[str, Any]],
+               wait: float = 0.0) -&gt; None:
+    &#34;&#34;&#34;Test configuration of ControlPi system.
+
+    Setup message bus, process given configuration, run message bus and
+    plugins concurrently, send given messages on message bus and print all
+    messages on message bus. Terminate when queue of message bus is empty.
+
+    This function allows to test single plugins or small plugin
+    configurations with minimal boilerplate code:
+    &gt;&gt;&gt; asyncio.run(test(
+    ...     {&#34;Example Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                       &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                                     &#34;content&#34;: &#34;Test Message&#34;},
+    ...                                    {&#34;id&#34;: 42.42,
+    ...                                     &#34;content&#34;: &#34;Second Message&#34;}]}},
+    ...     [{&#34;target&#34;: &#34;Example Init&#34;,
+    ...       &#34;command&#34;: &#34;execute&#34;}]))  # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Example Init&#39;, &#39;plugin&#39;: &#39;Init&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Test Message&#39;}},
+                       {&#39;id&#39;: {&#39;const&#39;: 42.42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Second Message&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Example Init&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Example Init&#39;,
+             &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Example Init&#39;,
+             &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+
+    Similar functionality could be reached by using the Log and Init plugins
+    to print messages and send some messages on the bus, but these would
+    clutter the test configuration and code to stop the indefinitely running
+    bus would have to be added to each and every test.
+
+    Incorrect plugin configurations can also be tested by this:
+    &gt;&gt;&gt; asyncio.run(test(
+    ...     {&#34;Example Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;}}, []))
+    &#39;messages&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;messages&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                     &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;messages&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Init&#39;}
+    Configuration for &#39;Example Init&#39; is not valid.
+    &#34;&#34;&#34;
+    message_bus = MessageBus()
+
+    async def log(message):
+        if (&#39;sender&#39; in message and message[&#39;sender&#39;] == &#39;&#39; and
+                &#39;event&#39; in message and message[&#39;event&#39;] == &#39;registered&#39; and
+                &#39;client&#39; in message and message[&#39;client&#39;] == &#39;test()&#39;):
+            # Do not log own registration of &#39;test()&#39;:
+            return
+        print(f&#34;test(): {message}&#34;)
+    message_bus.register(&#39;test()&#39;, &#39;Test&#39;,
+                         [MessageTemplate()], [MessageTemplate()], log)
+
+    coroutines = _process_conf(message_bus, conf)
+    for coroutine in coroutines:
+        asyncio.create_task(coroutine)
+        # Give the created task opportunity to run:
+        await asyncio.sleep(0)
+    for message in messages:
+        await message_bus.send(Message(&#39;test()&#39;, message))
+        # Give immediate reactions to messages opportunity to happen:
+        await asyncio.sleep(0)
+    await asyncio.sleep(wait)
+    await message_bus._queue.join()</code></pre>
+</details>
+</dd>
+</dl>
+</section>
+<section>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3><a href="#header-submodules">Sub-modules</a></h3>
+<ul>
+<li><code><a title="controlpi.baseplugin" href="baseplugin.html">controlpi.baseplugin</a></code></li>
+<li><code><a title="controlpi.messagebus" href="messagebus.html">controlpi.messagebus</a></code></li>
+<li><code><a title="controlpi.pluginregistry" href="pluginregistry.html">controlpi.pluginregistry</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-functions">Functions</a></h3>
+<ul class="">
+<li><code><a title="controlpi.run" href="#controlpi.run">run</a></code></li>
+<li><code><a title="controlpi.test" href="#controlpi.test">test</a></code></li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi/messagebus.html b/doc/controlpi/messagebus.html
new file mode 100644 (file)
index 0000000..46bea91
--- /dev/null
@@ -0,0 +1,4244 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi.messagebus API documentation</title>
+<meta name="description" content="Provide an asynchronous message bus …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi.messagebus</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide an asynchronous message bus.</p>
+<p>A message is a dictionary with string keys and string, integer, float,
+Boolean, dictionary, or list values, where the inner dictionaries again
+have string keys and these values and the inner lists also have elements of
+these types. All messages have a special key 'sender' with the name of the
+sending client as string value, which is set by the constructor:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender', {'key 1': 'value 1'})
+&gt;&gt;&gt; m['key 2'] = 'value 2'
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
+</code></pre>
+<p>A message template is a mapping from string keys to JSON schemas as values.
+A message template matches a message if all keys of the template are
+contained in the message and the values in the message validate against the
+respective schemas. An empty mapping therefore matches all messages.</p>
+<p>The bus executes asynchronous callbacks for all messages to be received by
+a client. We use a simple callback printing the message in all examples:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; def callback_for_receiver(receiver):
+...     async def callback(message):
+...         print(f&quot;{receiver}: {message}&quot;)
+...     return callback
+</code></pre>
+<p>Clients can be registered at the bus with a name, lists of message templates
+they want to use for sending and receiving and a callback function for
+receiving. An empty list of templates means that the client does not want to
+send or receive any messages, respectively. A list with an empty template
+means that it wants to send arbitrary or receive all messages, respectively:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def setup(bus):
+...     bus.register('Logger', 'Test Plugin',
+...                  [],
+...                  [{}],
+...                  callback_for_receiver('Logger'))
+...     bus.register('Client 1', 'Test Plugin',
+...                  [{'k1': {'type': 'string'}}],
+...                  [{'target': {'const': 'Client 1'}}],
+...                  callback_for_receiver('Client 1'))
+</code></pre>
+<p>While most clients should always use their own name for sending, this is not
+enforced and debugging or management clients could send messages on behalf
+of arbitrary client names.</p>
+<p>The name of a client has to be unique and is not allowed to be empty
+(otherwise registration fails).</p>
+<p>The empty name is used to refer to the bus itself. The bus sends messages
+for registrations and deregistrations of clients containing their complete
+interface of send and receive templates. This can be used to allow dynamic
+(debug) clients to deal with arbitrary configurations of clients. The bus
+also reacts to 'get clients' command messages by sending the complete
+information of all currently registered clients.</p>
+<p>Clients can send to the bus with the send function. Each message has to
+declare a sender. The send templates of that sender are checked for a
+template matching the message:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def send(bus):
+...     print(&quot;Sending messages.&quot;)
+...     await bus.send({'sender': 'Client 1', 'k1': 'Test'})
+...     await bus.send({'sender': '', 'target': 'Client 1'})
+</code></pre>
+<p>The run function executes the message bus forever. If we want to stop it, we
+have to explicitly cancel the task:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     await setup(bus)
+...     bus_task = asyncio.create_task(bus.run())
+...     await send(bus)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+Sending messages.
+Logger: {'sender': '', 'event': 'registered',
+         'client': 'Logger', 'plugin': 'Test Plugin',
+         'sends': [], 'receives': [{}]}
+Logger: {'sender': '', 'event': 'registered',
+         'client': 'Client 1', 'plugin': 'Test Plugin',
+         'sends': [{'k1': {'type': 'string'}}],
+         'receives': [{'target': {'const': 'Client 1'}}]}
+Logger: {'sender': 'Client 1', 'k1': 'Test'}
+Logger: {'sender': '', 'target': 'Client 1'}
+Client 1: {'sender': '', 'target': 'Client 1'}
+</code></pre>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide an asynchronous message bus.
+
+A message is a dictionary with string keys and string, integer, float,
+Boolean, dictionary, or list values, where the inner dictionaries again
+have string keys and these values and the inner lists also have elements of
+these types. All messages have a special key &#39;sender&#39; with the name of the
+sending client as string value, which is set by the constructor:
+&gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;})
+&gt;&gt;&gt; m[&#39;key 2&#39;] = &#39;value 2&#39;
+&gt;&gt;&gt; print(m)
+{&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+
+A message template is a mapping from string keys to JSON schemas as values.
+A message template matches a message if all keys of the template are
+contained in the message and the values in the message validate against the
+respective schemas. An empty mapping therefore matches all messages.
+
+The bus executes asynchronous callbacks for all messages to be received by
+a client. We use a simple callback printing the message in all examples:
+&gt;&gt;&gt; def callback_for_receiver(receiver):
+...     async def callback(message):
+...         print(f&#34;{receiver}: {message}&#34;)
+...     return callback
+
+Clients can be registered at the bus with a name, lists of message templates
+they want to use for sending and receiving and a callback function for
+receiving. An empty list of templates means that the client does not want to
+send or receive any messages, respectively. A list with an empty template
+means that it wants to send arbitrary or receive all messages, respectively:
+&gt;&gt;&gt; async def setup(bus):
+...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+...                  [],
+...                  [{}],
+...                  callback_for_receiver(&#39;Logger&#39;))
+...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+...                  callback_for_receiver(&#39;Client 1&#39;))
+
+While most clients should always use their own name for sending, this is not
+enforced and debugging or management clients could send messages on behalf
+of arbitrary client names.
+
+The name of a client has to be unique and is not allowed to be empty
+(otherwise registration fails).
+
+The empty name is used to refer to the bus itself. The bus sends messages
+for registrations and deregistrations of clients containing their complete
+interface of send and receive templates. This can be used to allow dynamic
+(debug) clients to deal with arbitrary configurations of clients. The bus
+also reacts to &#39;get clients&#39; command messages by sending the complete
+information of all currently registered clients.
+
+Clients can send to the bus with the send function. Each message has to
+declare a sender. The send templates of that sender are checked for a
+template matching the message:
+&gt;&gt;&gt; async def send(bus):
+...     print(&#34;Sending messages.&#34;)
+...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;})
+...     await bus.send({&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+
+The run function executes the message bus forever. If we want to stop it, we
+have to explicitly cancel the task:
+&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     await setup(bus)
+...     bus_task = asyncio.create_task(bus.run())
+...     await send(bus)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+Sending messages.
+Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Logger&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+         &#39;sends&#39;: [], &#39;receives&#39;: [{}]}
+Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Client 1&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+         &#39;sends&#39;: [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}]}
+Logger: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;}
+Logger: {&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+Client 1: {&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+&#34;&#34;&#34;
+import asyncio
+import json
+import jsonschema  # type: ignore
+
+from typing import Union, Dict, List, Any, Iterable, Callable, Coroutine
+MessageValue = Union[None, str, int, float, bool, Dict[str, Any], List[Any]]
+# Should really be:
+# MessageValue = Union[None, str, int, float, bool,
+#                      Dict[str, &#39;MessageValue&#39;], List[&#39;MessageValue&#39;]]
+# But mypy does not support recursion by now:
+# https://github.com/python/mypy/issues/731
+JSONSchema = Union[bool, Dict[str, MessageValue]]
+# Could be even more specific.
+MessageCallback = Callable[[&#39;Message&#39;], Coroutine[Any, Any, None]]
+
+
+class Message(Dict[str, MessageValue]):
+    &#34;&#34;&#34;Define arbitrary message.
+
+    Messages are dictionaries with string keys and values that are strings,
+    integers, floats, Booleans, dictionaries that recursively have string
+    keys and values of any of these types, or lists with elements that have
+    any of these types. These constraints are checked when setting key-value
+    pairs of the message.
+
+    A message has to have a sender, which is set by the constructor:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;}
+
+    A dictionary can be given to the constructor:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;})
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+
+    Or the message can be modified after construction:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;})
+    &gt;&gt;&gt; m[&#39;key 2&#39;] = &#39;value 2&#39;
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+    &#34;&#34;&#34;
+
+    def __init__(self, sender: str,
+                 init: Dict[str, MessageValue] = None) -&gt; None:
+        &#34;&#34;&#34;Initialise message.
+
+        Message is initialised with given sender and possibly given
+        key-value pairs:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;}
+
+        The sender can be overwritten by the key-value pairs:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;sender&#39;: &#39;Another sender&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Another sender&#39;}
+        &#34;&#34;&#34;
+        if not isinstance(sender, str):
+            raise TypeError(f&#34;&#39;{sender}&#39; is not a valid sender name&#34;
+                            &#34; (not a string).&#34;)
+        self[&#39;sender&#39;] = sender
+        if init is not None:
+            self.update(init)
+
+    @staticmethod
+    def check_value(value: MessageValue) -&gt; bool:
+        &#34;&#34;&#34;Check recursively if a given value is valid.
+
+        None, strings, integers, floats and Booleans are valid:
+        &gt;&gt;&gt; Message.check_value(None)
+        True
+        &gt;&gt;&gt; Message.check_value(&#39;Spam&#39;)
+        True
+        &gt;&gt;&gt; Message.check_value(42)
+        True
+        &gt;&gt;&gt; Message.check_value(42.42)
+        True
+        &gt;&gt;&gt; Message.check_value(False)
+        True
+
+        Other basic types are not valid:
+        &gt;&gt;&gt; Message.check_value(b&#39;bytes&#39;)
+        False
+        &gt;&gt;&gt; Message.check_value(1j)
+        False
+
+        Dictionaries with string keys and recursively valid values are valid:
+        &gt;&gt;&gt; Message.check_value({&#39;str value&#39;: &#39;Spam&#39;, &#39;int value&#39;: 42,
+        ...                      &#39;float value&#39;: 42.42, &#39;bool value&#39;: False})
+        True
+
+        Empty dictionaries are valid:
+        &gt;&gt;&gt; Message.check_value({})
+        True
+
+        Dictionaries with other keys are not valid:
+        &gt;&gt;&gt; Message.check_value({42: &#39;int key&#39;})
+        False
+
+        Dictionaries with invalid values are not valid:
+        &gt;&gt;&gt; Message.check_value({&#39;complex value&#39;: 1j})
+        False
+
+        Lists with valid elements are valid:
+        &gt;&gt;&gt; Message.check_value([&#39;Spam&#39;, 42, 42.42, False])
+        True
+
+        Empty lists are valid:
+        &gt;&gt;&gt; Message.check_value([])
+        True
+
+        Lists with invalid elements are not valid:
+        &gt;&gt;&gt; Message.check_value([1j])
+        False
+        &#34;&#34;&#34;
+        if value is None:
+            return True
+        elif (isinstance(value, str) or isinstance(value, int) or
+                isinstance(value, float) or isinstance(value, bool)):
+            return True
+        elif isinstance(value, dict):
+            for key in value:
+                if not isinstance(key, str):
+                    return False
+                if not Message.check_value(value[key]):
+                    return False
+            return True
+        elif isinstance(value, list):
+            for element in value:
+                if not Message.check_value(element):
+                    return False
+            return True
+        return False
+
+    def __setitem__(self, key: str, value: MessageValue) -&gt; None:
+        &#34;&#34;&#34;Check key and value before putting pair into dict.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m[&#39;key&#39;] = &#39;value&#39;
+        &gt;&gt;&gt; m[&#39;dict&#39;] = {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}
+        &gt;&gt;&gt; print(m)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key&#39;: &#39;value&#39;,
+         &#39;dict&#39;: {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m[42] = &#39;int key&#39;
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m[&#39;complex value&#39;] = 1j
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+        &#34;&#34;&#34;
+        if not isinstance(key, str):
+            raise TypeError(f&#34;&#39;{key}&#39; is not a valid key in Message&#34;
+                            &#34; (not a string).&#34;)
+        if not self.check_value(value):
+            raise TypeError(f&#34;&#39;{value}&#39; is not a valid value in Message.&#34;)
+        super().__setitem__(key, value)
+
+    def update(self, *args, **kwargs) -&gt; None:
+        &#34;&#34;&#34;Override update to use validity checks.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m.update({&#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+        &gt;&gt;&gt; m.update({42: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m.update({&#39;complex value&#39;: 1j})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+
+        This is also used in __init__:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {42: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;complex value&#39;: 1j})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+        &#34;&#34;&#34;
+        if args:
+            if len(args) &gt; 1:
+                raise TypeError(&#34;update expected at most 1 argument,&#34;
+                                f&#34; got {len(args)}&#34;)
+            other = dict(args[0])
+            for key in other:
+                self[key] = other[key]
+        for key in kwargs:
+            self[key] = kwargs[key]
+
+    def setdefault(self, key: str, value: MessageValue = None) -&gt; MessageValue:
+        &#34;&#34;&#34;Override setdefault to use validity checks.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 1&#39;)
+        &#39;value 1&#39;
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 2&#39;)
+        &#39;value 1&#39;
+        &gt;&gt;&gt; m.setdefault(42, &#39;int key&#39;)
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m.setdefault(&#39;complex value&#39;, 1j)
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+
+        But __setitem__ is not called if the key is already present:
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, 1j)
+        &#39;value 1&#39;
+        &#34;&#34;&#34;
+        if key not in self:
+            self[key] = value
+        return self[key]
+
+
+class MessageTemplate(Dict[str, JSONSchema]):
+    &#34;&#34;&#34;Define a message template.
+
+    A message template is a mapping from string keys to JSON schemas as
+    values:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+    ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;}})
+    &gt;&gt;&gt; t[&#39;key 3&#39;] = {&#39;type&#39;: &#39;object&#39;,
+    ...               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                              &#39;key 2&#39;: True}}
+
+    A message template matches a message if all keys of the template are
+    contained in the message and the values in the message validate against
+    the respective schemas:
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+    ...                  &#39;key 3&#39;: {&#39;key 1&#39;: 42, &#39;key 2&#39;: None}}))
+    True
+
+    An empty mapping therefore matches all messages:
+    &gt;&gt;&gt; t = MessageTemplate()
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;arbitrary&#39;: &#39;content&#39;}))
+    True
+    &#34;&#34;&#34;
+
+    def __init__(self, init: Dict[str, JSONSchema] = None) -&gt; None:
+        &#34;&#34;&#34;Initialise message.
+
+        Template is initialised empty or with given key-value pairs:
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; print(t)
+        {}
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}})
+        &gt;&gt;&gt; print(t)
+        {&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}}
+        &#34;&#34;&#34;
+        if init is not None:
+            self.update(init)
+
+    @staticmethod
+    def from_message(message: Message) -&gt; &#39;MessageTemplate&#39;:
+        &#34;&#34;&#34;Create template from message.
+
+        Template witch constant schemas is created from message:
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; print(t)
+        {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;}, &#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}}
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+        ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;},
+         &#39;dict&#39;: {&#39;type&#39;: &#39;object&#39;,
+                  &#39;properties&#39;: {&#39;int&#39;: {&#39;const&#39;: 42},
+                                 &#39;float&#39;: {&#39;const&#39;: 42.42}}},
+         &#39;list&#39;: {&#39;type&#39;: &#39;array&#39;,
+                  &#39;items&#39;: [{&#39;const&#39;: None},
+                            {&#39;const&#39;: True},
+                            {&#39;const&#39;: &#39;string&#39;}]}}
+
+        This is especially useful for clients that send certain fully
+        predefined messages, where the message is given in the configuration
+        and the template for the registration can be constructed by this
+        method.
+        &#34;&#34;&#34;
+        def schema_from_value(value: MessageValue) -&gt; JSONSchema:
+            schema: JSONSchema = False
+            if value is None:
+                schema = {&#39;const&#39;: None}
+            elif (isinstance(value, str) or isinstance(value, int) or
+                    isinstance(value, float) or isinstance(value, bool)):
+                schema = {&#39;const&#39;: value}
+            elif isinstance(value, dict):
+                properties = {}
+                for inner_key in value:
+                    inner_value: Message = value[inner_key]
+                    properties[inner_key] = schema_from_value(inner_value)
+                schema = {&#39;type&#39;: &#39;object&#39;,
+                          &#39;properties&#39;: properties}
+            elif isinstance(value, list):
+                schema = {&#39;type&#39;: &#39;array&#39;,
+                          &#39;items&#39;: [schema_from_value(element)
+                                    for element in value]}
+            return schema
+        template = MessageTemplate()
+        for key in message:
+            template[key] = schema_from_value(message[key])
+        return template
+
+    def __setitem__(self, key: str, value: JSONSchema) -&gt; None:
+        &#34;&#34;&#34;Check key and value before putting pair into dict.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t[&#39;key 1&#39;] = {&#39;const&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; t[&#39;key 2&#39;] = {&#39;type&#39;: &#39;string&#39;}
+        &gt;&gt;&gt; t[&#39;key 3&#39;] = {&#39;type&#39;: &#39;object&#39;,
+        ...               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                              &#39;key 2&#39;: True}}
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t[42] = {&#39;const&#39;: &#39;int key&#39;}
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t[&#39;key&#39;] = &#39;schema&#39;  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+        &#34;&#34;&#34;
+        if not isinstance(key, str):
+            raise TypeError(f&#34;&#39;{key}&#39; is not a valid key in MessageTemplate&#34;
+                            &#34; (not a string).&#34;)
+        try:
+            jsonschema.Draft7Validator.check_schema(value)
+            # Draft7Validator is hardcoded, because _LATEST_VERSION is
+            # non-public in jsonschema and we also perhaps do not want to
+            # upgrade automatically.
+        except jsonschema.exceptions.SchemaError:
+            raise TypeError(f&#34;&#39;{value}&#39; is not a valid value in&#34;
+                            &#34; MessageTemplate (not a valid JSON schema).&#34;)
+        super().__setitem__(key, value)
+
+    def update(self, *args, **kwargs) -&gt; None:
+        &#34;&#34;&#34;Override update to use validity checks.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t.update({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...           &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...           &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                     &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True}}})
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t.update({42: {&#39;const&#39;: &#39;int key&#39;}})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t.update({&#39;key&#39;: &#39;schema&#39;})  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+
+        This is also used in __init__:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                                &#39;properties&#39;: {
+        ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True}}})
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t = MessageTemplate({42: {&#39;const&#39;: &#39;int key&#39;}})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: &#39;schema&#39;})
+        ... # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+        &#34;&#34;&#34;
+        if args:
+            if len(args) &gt; 1:
+                raise TypeError(&#34;update expected at most 1 argument,&#34;
+                                f&#34; got {len(args)}&#34;)
+            other = dict(args[0])
+            for key in other:
+                self[key] = other[key]
+        for key in kwargs:
+            self[key] = kwargs[key]
+
+    def setdefault(self, key: str, value: JSONSchema = None) -&gt; JSONSchema:
+        &#34;&#34;&#34;Override setdefault to use validity checks.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, {&#39;const&#39;: &#39;value&#39;})
+        {&#39;const&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; t.setdefault(&#39;key 2&#39;, {&#39;type&#39;: &#39;string&#39;})
+        {&#39;type&#39;: &#39;string&#39;}
+        &gt;&gt;&gt; t.setdefault(&#39;key 3&#39;, {&#39;type&#39;: &#39;object&#39;,
+        ...                        &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                       &#39;key 2&#39;: True}})
+        ... # doctest: +NORMALIZE_WHITESPACE
+        {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}
+        &gt;&gt;&gt; t.setdefault(42, {&#39;const&#39;: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t.setdefault(&#39;key&#39;, &#39;schema&#39;)  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+
+        But __setitem__ is not called if the key is already present:
+        &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, &#39;schema&#39;)
+        {&#39;const&#39;: &#39;value&#39;}
+        &#34;&#34;&#34;
+        if key not in self:
+            if value is not None:
+                self[key] = value
+            else:
+                self[key] = True
+        return self[key]
+
+    def check(self, message: Message) -&gt; bool:
+        &#34;&#34;&#34;Check message against this template.
+
+        Constant values have to match exactly:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;other value&#39;}))
+        False
+
+        But for integers, floats with the same value are also valid:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: 42}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+        True
+
+        Type integer is valid for floats with zero fractional part, but
+        not by floats with non-zero fractional part:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;integer&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+        False
+
+        Type number is valid for arbitrary ints or floats:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;number&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+        True
+
+        All keys in template have to be present in message:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                                &#39;properties&#39;: {
+        ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True,
+        ...                                    &#39;key 3&#39;: False}}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;}))
+        False
+
+        But for nested objects their properties do not necessarily have
+        to be present:
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 1&#39;: 42}}))
+        True
+
+        Schema True matches everything (even None):
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 2&#39;: None}}))
+        True
+
+        Schema False matches nothing:
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 3&#39;: True}}))
+        False
+
+        Message is valid for the constant template created from it:
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+        ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; t.check(m)
+        True
+        &#34;&#34;&#34;
+        for key in self:
+            if key not in message:
+                return False
+            else:
+                validator = jsonschema.Draft7Validator(self[key])
+                for error in validator.iter_errors(message[key]):
+                    return False
+        return True
+
+
+class MessageTemplateRegistry:
+    &#34;&#34;&#34;Manage a collection of message templates with registered clients.
+
+    A new MessageTemplateRegistry is created by:
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+
+    Client names (strings) can be registered for message templates, which
+    are mappings from keys to JSON schemas:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+
+    The check function checks if the templates registered for a client
+    match a given message:
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 1&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    Clients can be registered for values validating against arbitrary JSON
+    schemas, e.g. all values of a certain type:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}, &#39;C 2&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 2&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}, &#39;C 3&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 3&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    The order of key-value pairs does not have to match the order in the
+    messages and keys can be left out:
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: 2}}, &#39;C 4&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 4&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    A registration for an empty template matches all messages:
+    &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 5&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    A client can be registered for multiple templates:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 6&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 6&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 6&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    Clients can be deregistered again (the result is False if the registry
+    is empty after the deletion):
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 7&#39;)
+    &gt;&gt;&gt; r.delete(&#39;C 7&#39;)
+    True
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 7&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    The get function returns all clients with registered templates matching
+    a given message:
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.get(m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;C 5&#39;, &#39;C 1&#39;, &#39;C 6&#39;]
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: [&#39;C 5&#39;, &#39;C 1&#39;, &#39;C 6&#39;, &#39;C 4&#39;]
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;C 5&#39;, &#39;C 2&#39;, &#39;C 6&#39;]
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: [&#39;C 5&#39;, &#39;C 3&#39;, &#39;C 4&#39;]
+
+    The get_templates function returns all templates for a given client:
+    &gt;&gt;&gt; for c in [&#39;C 1&#39;, &#39;C 2&#39;, &#39;C 3&#39;, &#39;C 4&#39;, &#39;C 5&#39;, &#39;C 6&#39;]:
+    ...     print(f&#34;{c}: {r.get_templates(c)}&#34;)
+    C 1: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    C 2: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}]
+    C 3: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}]
+    C 4: [{&#39;k2&#39;: {&#39;const&#39;: 2}}]
+    C 5: [{}]
+    C 6: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, {&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    &#34;&#34;&#34;
+
+    def __init__(self) -&gt; None:
+        &#34;&#34;&#34;Initialise an empty registry.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &#34;&#34;&#34;
+        self._clients: List[str] = []
+        self._children: Dict[str, Dict[str, MessageTemplateRegistry]] = {}
+
+    def insert(self, template: MessageTemplate, client: str) -&gt; None:
+        &#34;&#34;&#34;Register a client for a template.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+        &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+
+        Implementation details:
+        -----------------------
+        The tree nodes on the way to a registered object are used/created
+        in the order given in the message template, which can be used to
+        design more efficient lookups (e.g., putting rarer key-value pairs
+        earlier in the template).
+        &gt;&gt;&gt; r._clients
+        [&#39;C 5&#39;]
+        &gt;&gt;&gt; r._children.keys()
+        dict_keys([&#39;k1&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 1&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 2&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 3&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 4&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &#34;&#34;&#34;
+        if not template:
+            self._clients.append(client)
+        else:
+            key, schema = next(iter(template.items()))
+            schema_string = json.dumps(schema)
+            reduced_template = MessageTemplate({k: template[k]
+                                                for k in template
+                                                if k != key})
+            if key not in self._children:
+                self._children[key] = {}
+            if schema_string not in self._children[key]:
+                self._children[key][schema_string] = MessageTemplateRegistry()
+            self._children[key][schema_string].insert(reduced_template, client)
+
+    def delete(self, client: str) -&gt; bool:
+        &#34;&#34;&#34;Unregister a client from all templates.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+        &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+        &gt;&gt;&gt; r.delete(&#39;C 3&#39;)
+        True
+        &gt;&gt;&gt; r.delete(&#39;C 4&#39;)
+        True
+
+        Implementation details:
+        -----------------------
+        If parts of the tree become superfluous by the deletion of the
+        client, they are also completely removed to reduce the lookup
+        effort and keep the tree clean.
+        &gt;&gt;&gt; r._clients
+        [&#39;C 5&#39;]
+        &gt;&gt;&gt; r._children.keys()
+        dict_keys([&#39;k1&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 1&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 2&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &#34;&#34;&#34;
+        self._clients = [c for c in self._clients if c != client]
+        new_children: Dict[str, Dict[str, MessageTemplateRegistry]] = {}
+        for key in self._children:
+            new_children[key] = {}
+            for schema in self._children[key]:
+                if self._children[key][schema].delete(client):
+                    new_children[key][schema] = self._children[key][schema]
+            if not new_children[key]:
+                del new_children[key]
+        self._children = new_children
+        if self._clients or self._children:
+            return True
+        return False
+
+    def check(self, client: str, message: Message) -&gt; bool:
+        &#34;&#34;&#34;Get if a client has a registered template matching a message.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.check(&#39;Client 1&#39;, m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: False
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.check(&#39;Client 2&#39;, m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        &#34;&#34;&#34;
+        if client in self._clients:
+            return True
+        for key in self._children:
+            if key in message:
+                for schema_string in self._children[key]:
+                    schema = json.loads(schema_string)
+                    validator = jsonschema.Draft7Validator(schema)
+                    validated = True
+                    for error in validator.iter_errors(message[key]):
+                        validated = False
+                    if validated:
+                        child = self._children[key][schema_string]
+                        if child.check(client, message):
+                            return True
+        return False
+
+    def get(self, message: Message) -&gt; List[str]:
+        &#34;&#34;&#34;Get all clients registered for templates matching a message.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.get(m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;Client 1&#39;]
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 1&#39;, &#39;Client 2&#39;]
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: []
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 2&#39;]
+        &#34;&#34;&#34;
+        result = []
+        for client in self._clients:
+            if client not in result:
+                result.append(client)
+        for key in self._children:
+            if key in message:
+                for schema_string in self._children[key]:
+                    schema = json.loads(schema_string)
+                    validator = jsonschema.Draft7Validator(schema)
+                    validated = True
+                    for error in validator.iter_errors(message[key]):
+                        validated = False
+                    if validated:
+                        child = self._children[key][schema_string]
+                        for client in child.get(message):
+                            if client not in result:
+                                result.append(client)
+        return result
+
+    def get_templates(self, client: str) -&gt; List[MessageTemplate]:
+        &#34;&#34;&#34;Get all templates for a client.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 1&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+        ...           &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 2&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+        ...           &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}, &#39;Client 3&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 3&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: 2}}, &#39;Client 4&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 4&#39;)
+        [{&#39;k2&#39;: {&#39;const&#39;: 2}}]
+        &gt;&gt;&gt; r.insert({}, &#39;Client 5&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 5&#39;)
+        [{}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 6&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, {&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+        &#34;&#34;&#34;
+        result = []
+        if client in self._clients:
+            result.append(MessageTemplate())
+        for key in self._children:
+            for schema_string in self._children[key]:
+                schema = json.loads(schema_string)
+                child = self._children[key][schema_string]
+                for template in child.get_templates(client):
+                    current = MessageTemplate({key: schema})
+                    current.update(template)
+                    result.append(current)
+        return result
+
+
+class BusException(Exception):
+    &#34;&#34;&#34;Raise for errors in using message bus.&#34;&#34;&#34;
+
+
+class MessageBus:
+    &#34;&#34;&#34;Provide an asynchronous message bus.
+
+    The bus executes asynchronous callbacks for all messages to be received
+    by a client. We use a simple callback printing the message in all
+    examples:
+    &gt;&gt;&gt; def callback_for_receiver(receiver):
+    ...     print(f&#34;Creating callback for {receiver}.&#34;)
+    ...     async def callback(message):
+    ...         print(f&#34;{receiver}: {message}&#34;)
+    ...     return callback
+
+    Clients can be registered at the bus with a name, lists of message
+    templates they want to use for sending and receiving and a callback
+    function for receiving. An empty list of templates means that the
+    client does not want to send or receive any messages, respectively.
+    A list with an empty template means that it wants to send arbitrary
+    or receive all messages, respectively:
+    &gt;&gt;&gt; async def setup(bus):
+    ...     print(&#34;Setting up.&#34;)
+    ...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+    ...                  [],
+    ...                  [{}],
+    ...                  callback_for_receiver(&#39;Logger&#39;))
+    ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+    ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+    ...                  callback_for_receiver(&#39;Client 1&#39;))
+    ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+    ...                  [{}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+    ...                  callback_for_receiver(&#39;Client 2&#39;))
+
+    The bus itself is addressed by the empty string. It sends messages for
+    each registration and deregestration of a client with a key &#39;event&#39; and
+    a value of &#39;registered&#39; or &#39;unregistered&#39;, a key &#39;client&#39; with the
+    client&#39;s name as value and for registrations also keys &#39;sends&#39; and
+    &#39;receives&#39; with all templates registered for the client for sending and
+    receiving.
+
+    Clients can send to the bus with the send function. Each message has to
+    declare a sender. The send templates of that sender are checked for a
+    template matching the message. We cannot prevent arbitrary code from
+    impersonating any sender, but this should only be done in debugging or
+    management situations.
+
+    Messages that are intended for a specific client by convention have a
+    key &#39;target&#39; with the target client&#39;s name as value. Such messages are
+    often commands to the client to do something, which is by convention
+    indicated by a key &#39;command&#39; with a value that indicates what should be
+    done.
+
+    The bus, for example, reacts to a message with &#39;target&#39;: &#39;&#39; and
+    &#39;command&#39;: &#39;get clients&#39; by sending one message for each currently
+    registered with complete information about its registered send and
+    receive templates.
+
+    &gt;&gt;&gt; async def send(bus):
+    ...     print(&#34;Sending messages.&#34;)
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;})
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+    ...     await bus.send({&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;&#39;,
+    ...                     &#39;command&#39;: &#39;get clients&#39;})
+
+    The run function executes the message bus forever. If we want to stop
+    it, we have to explicitly cancel the task:
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     await setup(bus)
+    ...     bus_task = asyncio.create_task(bus.run())
+    ...     await send(bus)
+    ...     await asyncio.sleep(0)
+    ...     bus_task.cancel()
+    &gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+    Setting up.
+    Creating callback for Logger.
+    Creating callback for Client 1.
+    Creating callback for Client 2.
+    Sending messages.
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Logger&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Client 1&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Client 2&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{}], &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;}
+    Logger: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+    Client 1: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;&#39;, &#39;command&#39;: &#39;get clients&#39;}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Logger&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Client 1&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Client 2&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{}], &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}]}
+    &#34;&#34;&#34;
+
+    def __init__(self) -&gt; None:
+        &#34;&#34;&#34;Initialise a new bus without clients.
+
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        self._queue: asyncio.Queue = asyncio.Queue()
+        self._plugins: Dict[str, str] = {}
+        self._send_reg: MessageTemplateRegistry = MessageTemplateRegistry()
+        self._recv_reg: MessageTemplateRegistry = MessageTemplateRegistry()
+        self._callbacks: Dict[str, MessageCallback] = {}
+
+    def register(self, client: str, plugin: str,
+                 sends: Iterable[MessageTemplate],
+                 receives: Iterable[MessageTemplate],
+                 callback: MessageCallback) -&gt; None:
+        &#34;&#34;&#34;Register a client at the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(message)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+        ...                  [],    # send nothing
+        ...                  [{}],  # receive everything
+        ...                  callback)
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                      # send with key &#39;k1&#39; and string value
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                      # receive for this client
+        ...                  callback)
+        ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+        ...                  [{}],  # send arbitrary
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+        ...                      # receive for this client
+        ...                  callback)
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        if not client:
+            raise BusException(&#34;Client name is not allowed to be empty.&#34;)
+        if client in self._plugins:
+            raise BusException(f&#34;Client &#39;{client}&#39; already registered&#34;
+                               &#34; at message bus.&#34;)
+        event = Message(&#39;&#39;)
+        event[&#39;event&#39;] = &#39;registered&#39;
+        event[&#39;client&#39;] = client
+        self._plugins[client] = plugin
+        event[&#39;plugin&#39;] = plugin
+        for template in sends:
+            self._send_reg.insert(template, client)
+        event[&#39;sends&#39;] = self._send_reg.get_templates(client)
+        for template in receives:
+            self._recv_reg.insert(template, client)
+        event[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+        self._callbacks[client] = callback
+        self._queue.put_nowait(event)
+
+    def unregister(self, client: str) -&gt; None:
+        &#34;&#34;&#34;Unregister a client from the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(message)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                  callback)
+        ...     bus.unregister(&#39;Client 1&#39;)
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        if client not in self._plugins:
+            return
+        event = Message(&#39;&#39;)
+        event[&#39;event&#39;] = &#39;unregistered&#39;
+        event[&#39;client&#39;] = client
+        del self._plugins[client]
+        self._send_reg.delete(client)
+        self._recv_reg.delete(client)
+        if client in self._callbacks:
+            del self._callbacks[client]
+        self._queue.put_nowait(event)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run the message bus forever.
+
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus_task = asyncio.create_task(bus.run())
+        ...     bus_task.cancel()
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        while True:
+            message = await self._queue.get()
+            if (&#39;target&#39; in message and
+                    message[&#39;target&#39;] == &#39;&#39; and
+                    &#39;command&#39; in message and
+                    message[&#39;command&#39;] == &#39;get clients&#39;):
+                for client in self._plugins:
+                    answer = Message(&#39;&#39;)
+                    answer[&#39;client&#39;] = client
+                    answer[&#39;plugin&#39;] = self._plugins[client]
+                    answer[&#39;sends&#39;] = self._send_reg.get_templates(client)
+                    answer[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+                    await self._queue.put(answer)
+            for client in self._recv_reg.get(message):
+                await self._callbacks[client](message)
+            self._queue.task_done()
+
+    async def send(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Send a message to the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(f&#34;Got: {message}&#34;)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                  callback)
+        ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+        ...                  [{}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+        ...                  callback)
+        ...     bus_task = asyncio.create_task(bus.run())
+        ...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+        ...                     &#39;k1&#39;: &#39;Test&#39;})
+        ...     await bus.send({&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+        ...     try:
+        ...         await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+        ...                         &#39;k1&#39;: 42})
+        ...     except BusException as e:
+        ...         print(e)
+        ...     await asyncio.sleep(0)
+        ...     bus_task.cancel()
+        &gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+        Message &#39;{&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: 42}&#39;
+        not allowed for sender &#39;Client 1&#39;.
+        Got: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: &#39;Test&#39;}
+        Got: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+        &#34;&#34;&#34;
+        assert isinstance(message[&#39;sender&#39;], str)
+        sender = message[&#39;sender&#39;]
+        if sender:
+            if not self._send_reg.check(sender, message):
+                raise BusException(f&#34;Message &#39;{message}&#39; not allowed for&#34;
+                                   f&#34; sender &#39;{sender}&#39;.&#34;)
+        await self._queue.put(message)</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi.messagebus.Message"><code class="flex name class">
+<span>class <span class="ident">Message</span></span>
+<span>(</span><span>sender: str, init: Dict[str, Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]] = None)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Define arbitrary message.</p>
+<p>Messages are dictionaries with string keys and values that are strings,
+integers, floats, Booleans, dictionaries that recursively have string
+keys and values of any of these types, or lists with elements that have
+any of these types. These constraints are checked when setting key-value
+pairs of the message.</p>
+<p>A message has to have a sender, which is set by the constructor:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender')
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender'}
+</code></pre>
+<p>A dictionary can be given to the constructor:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender', {'key 1': 'value 1', 'key 2': 'value 2'})
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
+</code></pre>
+<p>Or the message can be modified after construction:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender', {'key 1': 'value 1'})
+&gt;&gt;&gt; m['key 2'] = 'value 2'
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
+</code></pre>
+<p>Initialise message.</p>
+<p>Message is initialised with given sender and possibly given
+key-value pairs:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender')
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender'}
+&gt;&gt;&gt; m = Message('Example sender', {'key 1': 'value 1'})
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key 1': 'value 1'}
+</code></pre>
+<p>The sender can be overwritten by the key-value pairs:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender', {'sender': 'Another sender'})
+&gt;&gt;&gt; print(m)
+{'sender': 'Another sender'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Message(Dict[str, MessageValue]):
+    &#34;&#34;&#34;Define arbitrary message.
+
+    Messages are dictionaries with string keys and values that are strings,
+    integers, floats, Booleans, dictionaries that recursively have string
+    keys and values of any of these types, or lists with elements that have
+    any of these types. These constraints are checked when setting key-value
+    pairs of the message.
+
+    A message has to have a sender, which is set by the constructor:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;}
+
+    A dictionary can be given to the constructor:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;})
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+
+    Or the message can be modified after construction:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;})
+    &gt;&gt;&gt; m[&#39;key 2&#39;] = &#39;value 2&#39;
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+    &#34;&#34;&#34;
+
+    def __init__(self, sender: str,
+                 init: Dict[str, MessageValue] = None) -&gt; None:
+        &#34;&#34;&#34;Initialise message.
+
+        Message is initialised with given sender and possibly given
+        key-value pairs:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key 1&#39;: &#39;value 1&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;}
+
+        The sender can be overwritten by the key-value pairs:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;sender&#39;: &#39;Another sender&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Another sender&#39;}
+        &#34;&#34;&#34;
+        if not isinstance(sender, str):
+            raise TypeError(f&#34;&#39;{sender}&#39; is not a valid sender name&#34;
+                            &#34; (not a string).&#34;)
+        self[&#39;sender&#39;] = sender
+        if init is not None:
+            self.update(init)
+
+    @staticmethod
+    def check_value(value: MessageValue) -&gt; bool:
+        &#34;&#34;&#34;Check recursively if a given value is valid.
+
+        None, strings, integers, floats and Booleans are valid:
+        &gt;&gt;&gt; Message.check_value(None)
+        True
+        &gt;&gt;&gt; Message.check_value(&#39;Spam&#39;)
+        True
+        &gt;&gt;&gt; Message.check_value(42)
+        True
+        &gt;&gt;&gt; Message.check_value(42.42)
+        True
+        &gt;&gt;&gt; Message.check_value(False)
+        True
+
+        Other basic types are not valid:
+        &gt;&gt;&gt; Message.check_value(b&#39;bytes&#39;)
+        False
+        &gt;&gt;&gt; Message.check_value(1j)
+        False
+
+        Dictionaries with string keys and recursively valid values are valid:
+        &gt;&gt;&gt; Message.check_value({&#39;str value&#39;: &#39;Spam&#39;, &#39;int value&#39;: 42,
+        ...                      &#39;float value&#39;: 42.42, &#39;bool value&#39;: False})
+        True
+
+        Empty dictionaries are valid:
+        &gt;&gt;&gt; Message.check_value({})
+        True
+
+        Dictionaries with other keys are not valid:
+        &gt;&gt;&gt; Message.check_value({42: &#39;int key&#39;})
+        False
+
+        Dictionaries with invalid values are not valid:
+        &gt;&gt;&gt; Message.check_value({&#39;complex value&#39;: 1j})
+        False
+
+        Lists with valid elements are valid:
+        &gt;&gt;&gt; Message.check_value([&#39;Spam&#39;, 42, 42.42, False])
+        True
+
+        Empty lists are valid:
+        &gt;&gt;&gt; Message.check_value([])
+        True
+
+        Lists with invalid elements are not valid:
+        &gt;&gt;&gt; Message.check_value([1j])
+        False
+        &#34;&#34;&#34;
+        if value is None:
+            return True
+        elif (isinstance(value, str) or isinstance(value, int) or
+                isinstance(value, float) or isinstance(value, bool)):
+            return True
+        elif isinstance(value, dict):
+            for key in value:
+                if not isinstance(key, str):
+                    return False
+                if not Message.check_value(value[key]):
+                    return False
+            return True
+        elif isinstance(value, list):
+            for element in value:
+                if not Message.check_value(element):
+                    return False
+            return True
+        return False
+
+    def __setitem__(self, key: str, value: MessageValue) -&gt; None:
+        &#34;&#34;&#34;Check key and value before putting pair into dict.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m[&#39;key&#39;] = &#39;value&#39;
+        &gt;&gt;&gt; m[&#39;dict&#39;] = {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}
+        &gt;&gt;&gt; print(m)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key&#39;: &#39;value&#39;,
+         &#39;dict&#39;: {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m[42] = &#39;int key&#39;
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m[&#39;complex value&#39;] = 1j
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+        &#34;&#34;&#34;
+        if not isinstance(key, str):
+            raise TypeError(f&#34;&#39;{key}&#39; is not a valid key in Message&#34;
+                            &#34; (not a string).&#34;)
+        if not self.check_value(value):
+            raise TypeError(f&#34;&#39;{value}&#39; is not a valid value in Message.&#34;)
+        super().__setitem__(key, value)
+
+    def update(self, *args, **kwargs) -&gt; None:
+        &#34;&#34;&#34;Override update to use validity checks.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m.update({&#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+        &gt;&gt;&gt; m.update({42: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m.update({&#39;complex value&#39;: 1j})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+
+        This is also used in __init__:
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+        &gt;&gt;&gt; print(m)
+        {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {42: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;complex value&#39;: 1j})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+        &#34;&#34;&#34;
+        if args:
+            if len(args) &gt; 1:
+                raise TypeError(&#34;update expected at most 1 argument,&#34;
+                                f&#34; got {len(args)}&#34;)
+            other = dict(args[0])
+            for key in other:
+                self[key] = other[key]
+        for key in kwargs:
+            self[key] = kwargs[key]
+
+    def setdefault(self, key: str, value: MessageValue = None) -&gt; MessageValue:
+        &#34;&#34;&#34;Override setdefault to use validity checks.
+
+        &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 1&#39;)
+        &#39;value 1&#39;
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 2&#39;)
+        &#39;value 1&#39;
+        &gt;&gt;&gt; m.setdefault(42, &#39;int key&#39;)
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+        &gt;&gt;&gt; m.setdefault(&#39;complex value&#39;, 1j)
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;1j&#39; is not a valid value in Message.
+
+        But __setitem__ is not called if the key is already present:
+        &gt;&gt;&gt; m.setdefault(&#39;key&#39;, 1j)
+        &#39;value 1&#39;
+        &#34;&#34;&#34;
+        if key not in self:
+            self[key] = value
+        return self[key]</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>builtins.dict</li>
+<li>typing.Generic</li>
+</ul>
+<h3>Static methods</h3>
+<dl>
+<dt id="controlpi.messagebus.Message.check_value"><code class="name flex">
+<span>def <span class="ident">check_value</span></span>(<span>value: Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]) ‑> bool</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Check recursively if a given value is valid.</p>
+<p>None, strings, integers, floats and Booleans are valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value(None)
+True
+&gt;&gt;&gt; Message.check_value('Spam')
+True
+&gt;&gt;&gt; Message.check_value(42)
+True
+&gt;&gt;&gt; Message.check_value(42.42)
+True
+&gt;&gt;&gt; Message.check_value(False)
+True
+</code></pre>
+<p>Other basic types are not valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value(b'bytes')
+False
+&gt;&gt;&gt; Message.check_value(1j)
+False
+</code></pre>
+<p>Dictionaries with string keys and recursively valid values are valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value({'str value': 'Spam', 'int value': 42,
+...                      'float value': 42.42, 'bool value': False})
+True
+</code></pre>
+<p>Empty dictionaries are valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value({})
+True
+</code></pre>
+<p>Dictionaries with other keys are not valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value({42: 'int key'})
+False
+</code></pre>
+<p>Dictionaries with invalid values are not valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value({'complex value': 1j})
+False
+</code></pre>
+<p>Lists with valid elements are valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value(['Spam', 42, 42.42, False])
+True
+</code></pre>
+<p>Empty lists are valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value([])
+True
+</code></pre>
+<p>Lists with invalid elements are not valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; Message.check_value([1j])
+False
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">@staticmethod
+def check_value(value: MessageValue) -&gt; bool:
+    &#34;&#34;&#34;Check recursively if a given value is valid.
+
+    None, strings, integers, floats and Booleans are valid:
+    &gt;&gt;&gt; Message.check_value(None)
+    True
+    &gt;&gt;&gt; Message.check_value(&#39;Spam&#39;)
+    True
+    &gt;&gt;&gt; Message.check_value(42)
+    True
+    &gt;&gt;&gt; Message.check_value(42.42)
+    True
+    &gt;&gt;&gt; Message.check_value(False)
+    True
+
+    Other basic types are not valid:
+    &gt;&gt;&gt; Message.check_value(b&#39;bytes&#39;)
+    False
+    &gt;&gt;&gt; Message.check_value(1j)
+    False
+
+    Dictionaries with string keys and recursively valid values are valid:
+    &gt;&gt;&gt; Message.check_value({&#39;str value&#39;: &#39;Spam&#39;, &#39;int value&#39;: 42,
+    ...                      &#39;float value&#39;: 42.42, &#39;bool value&#39;: False})
+    True
+
+    Empty dictionaries are valid:
+    &gt;&gt;&gt; Message.check_value({})
+    True
+
+    Dictionaries with other keys are not valid:
+    &gt;&gt;&gt; Message.check_value({42: &#39;int key&#39;})
+    False
+
+    Dictionaries with invalid values are not valid:
+    &gt;&gt;&gt; Message.check_value({&#39;complex value&#39;: 1j})
+    False
+
+    Lists with valid elements are valid:
+    &gt;&gt;&gt; Message.check_value([&#39;Spam&#39;, 42, 42.42, False])
+    True
+
+    Empty lists are valid:
+    &gt;&gt;&gt; Message.check_value([])
+    True
+
+    Lists with invalid elements are not valid:
+    &gt;&gt;&gt; Message.check_value([1j])
+    False
+    &#34;&#34;&#34;
+    if value is None:
+        return True
+    elif (isinstance(value, str) or isinstance(value, int) or
+            isinstance(value, float) or isinstance(value, bool)):
+        return True
+    elif isinstance(value, dict):
+        for key in value:
+            if not isinstance(key, str):
+                return False
+            if not Message.check_value(value[key]):
+                return False
+        return True
+    elif isinstance(value, list):
+        for element in value:
+            if not Message.check_value(element):
+                return False
+        return True
+    return False</code></pre>
+</details>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi.messagebus.Message.update"><code class="name flex">
+<span>def <span class="ident">update</span></span>(<span>self, *args, **kwargs) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Override update to use validity checks.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender')
+&gt;&gt;&gt; m.update({'key 1': 'value 1', 'key 2': 'value 2'})
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
+&gt;&gt;&gt; m.update({42: 'int key'})
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in Message (not a string).
+&gt;&gt;&gt; m.update({'complex value': 1j})
+Traceback (most recent call last):
+  ...
+TypeError: '1j' is not a valid value in Message.
+</code></pre>
+<p>This is also used in <strong>init</strong>:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender', {'key': 'value'})
+&gt;&gt;&gt; print(m)
+{'sender': 'Example sender', 'key': 'value'}
+&gt;&gt;&gt; m = Message('Example sender', {42: 'int key'})
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in Message (not a string).
+&gt;&gt;&gt; m = Message('Example sender', {'complex value': 1j})
+Traceback (most recent call last):
+  ...
+TypeError: '1j' is not a valid value in Message.
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def update(self, *args, **kwargs) -&gt; None:
+    &#34;&#34;&#34;Override update to use validity checks.
+
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+    &gt;&gt;&gt; m.update({&#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;})
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key 1&#39;: &#39;value 1&#39;, &#39;key 2&#39;: &#39;value 2&#39;}
+    &gt;&gt;&gt; m.update({42: &#39;int key&#39;})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+    &gt;&gt;&gt; m.update({&#39;complex value&#39;: 1j})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;1j&#39; is not a valid value in Message.
+
+    This is also used in __init__:
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+    &gt;&gt;&gt; print(m)
+    {&#39;sender&#39;: &#39;Example sender&#39;, &#39;key&#39;: &#39;value&#39;}
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {42: &#39;int key&#39;})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;, {&#39;complex value&#39;: 1j})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;1j&#39; is not a valid value in Message.
+    &#34;&#34;&#34;
+    if args:
+        if len(args) &gt; 1:
+            raise TypeError(&#34;update expected at most 1 argument,&#34;
+                            f&#34; got {len(args)}&#34;)
+        other = dict(args[0])
+        for key in other:
+            self[key] = other[key]
+    for key in kwargs:
+        self[key] = kwargs[key]</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.Message.setdefault"><code class="name flex">
+<span>def <span class="ident">setdefault</span></span>(<span>self, key: str, value: Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]] = None) ‑> Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Override setdefault to use validity checks.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example sender')
+&gt;&gt;&gt; m.setdefault('key', 'value 1')
+'value 1'
+&gt;&gt;&gt; m.setdefault('key', 'value 2')
+'value 1'
+&gt;&gt;&gt; m.setdefault(42, 'int key')
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in Message (not a string).
+&gt;&gt;&gt; m.setdefault('complex value', 1j)
+Traceback (most recent call last):
+  ...
+TypeError: '1j' is not a valid value in Message.
+</code></pre>
+<p>But <strong>setitem</strong> is not called if the key is already present:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m.setdefault('key', 1j)
+'value 1'
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def setdefault(self, key: str, value: MessageValue = None) -&gt; MessageValue:
+    &#34;&#34;&#34;Override setdefault to use validity checks.
+
+    &gt;&gt;&gt; m = Message(&#39;Example sender&#39;)
+    &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 1&#39;)
+    &#39;value 1&#39;
+    &gt;&gt;&gt; m.setdefault(&#39;key&#39;, &#39;value 2&#39;)
+    &#39;value 1&#39;
+    &gt;&gt;&gt; m.setdefault(42, &#39;int key&#39;)
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in Message (not a string).
+    &gt;&gt;&gt; m.setdefault(&#39;complex value&#39;, 1j)
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;1j&#39; is not a valid value in Message.
+
+    But __setitem__ is not called if the key is already present:
+    &gt;&gt;&gt; m.setdefault(&#39;key&#39;, 1j)
+    &#39;value 1&#39;
+    &#34;&#34;&#34;
+    if key not in self:
+        self[key] = value
+    return self[key]</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplate"><code class="flex name class">
+<span>class <span class="ident">MessageTemplate</span></span>
+<span>(</span><span>init: Dict[str, Union[bool, Dict[str, Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]]]] = None)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Define a message template.</p>
+<p>A message template is a mapping from string keys to JSON schemas as
+values:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key 1': {'const': 'value'},
+...                      'key 2': {'type': 'string'}})
+&gt;&gt;&gt; t['key 3'] = {'type': 'object',
+...               'properties': {'key 1': {'type': 'number'},
+...                              'key 2': True}}
+</code></pre>
+<p>A message template matches a message if all keys of the template are
+contained in the message and the values in the message validate against
+the respective schemas:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t.check(Message('Example Sender',
+...                 {'key 1': 'value', 'key 2': 'some string',
+...                  'key 3': {'key 1': 42, 'key 2': None}}))
+True
+</code></pre>
+<p>An empty mapping therefore matches all messages:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate()
+&gt;&gt;&gt; t.check(Message('Example Sender', {'arbitrary': 'content'}))
+True
+</code></pre>
+<p>Initialise message.</p>
+<p>Template is initialised empty or with given key-value pairs:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate()
+&gt;&gt;&gt; print(t)
+{}
+&gt;&gt;&gt; t = MessageTemplate({'key': {'const': 'value'}})
+&gt;&gt;&gt; print(t)
+{'key': {'const': 'value'}}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class MessageTemplate(Dict[str, JSONSchema]):
+    &#34;&#34;&#34;Define a message template.
+
+    A message template is a mapping from string keys to JSON schemas as
+    values:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+    ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;}})
+    &gt;&gt;&gt; t[&#39;key 3&#39;] = {&#39;type&#39;: &#39;object&#39;,
+    ...               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                              &#39;key 2&#39;: True}}
+
+    A message template matches a message if all keys of the template are
+    contained in the message and the values in the message validate against
+    the respective schemas:
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+    ...                  &#39;key 3&#39;: {&#39;key 1&#39;: 42, &#39;key 2&#39;: None}}))
+    True
+
+    An empty mapping therefore matches all messages:
+    &gt;&gt;&gt; t = MessageTemplate()
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;arbitrary&#39;: &#39;content&#39;}))
+    True
+    &#34;&#34;&#34;
+
+    def __init__(self, init: Dict[str, JSONSchema] = None) -&gt; None:
+        &#34;&#34;&#34;Initialise message.
+
+        Template is initialised empty or with given key-value pairs:
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; print(t)
+        {}
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}})
+        &gt;&gt;&gt; print(t)
+        {&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}}
+        &#34;&#34;&#34;
+        if init is not None:
+            self.update(init)
+
+    @staticmethod
+    def from_message(message: Message) -&gt; &#39;MessageTemplate&#39;:
+        &#34;&#34;&#34;Create template from message.
+
+        Template witch constant schemas is created from message:
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; print(t)
+        {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;}, &#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}}
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+        ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;},
+         &#39;dict&#39;: {&#39;type&#39;: &#39;object&#39;,
+                  &#39;properties&#39;: {&#39;int&#39;: {&#39;const&#39;: 42},
+                                 &#39;float&#39;: {&#39;const&#39;: 42.42}}},
+         &#39;list&#39;: {&#39;type&#39;: &#39;array&#39;,
+                  &#39;items&#39;: [{&#39;const&#39;: None},
+                            {&#39;const&#39;: True},
+                            {&#39;const&#39;: &#39;string&#39;}]}}
+
+        This is especially useful for clients that send certain fully
+        predefined messages, where the message is given in the configuration
+        and the template for the registration can be constructed by this
+        method.
+        &#34;&#34;&#34;
+        def schema_from_value(value: MessageValue) -&gt; JSONSchema:
+            schema: JSONSchema = False
+            if value is None:
+                schema = {&#39;const&#39;: None}
+            elif (isinstance(value, str) or isinstance(value, int) or
+                    isinstance(value, float) or isinstance(value, bool)):
+                schema = {&#39;const&#39;: value}
+            elif isinstance(value, dict):
+                properties = {}
+                for inner_key in value:
+                    inner_value: Message = value[inner_key]
+                    properties[inner_key] = schema_from_value(inner_value)
+                schema = {&#39;type&#39;: &#39;object&#39;,
+                          &#39;properties&#39;: properties}
+            elif isinstance(value, list):
+                schema = {&#39;type&#39;: &#39;array&#39;,
+                          &#39;items&#39;: [schema_from_value(element)
+                                    for element in value]}
+            return schema
+        template = MessageTemplate()
+        for key in message:
+            template[key] = schema_from_value(message[key])
+        return template
+
+    def __setitem__(self, key: str, value: JSONSchema) -&gt; None:
+        &#34;&#34;&#34;Check key and value before putting pair into dict.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t[&#39;key 1&#39;] = {&#39;const&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; t[&#39;key 2&#39;] = {&#39;type&#39;: &#39;string&#39;}
+        &gt;&gt;&gt; t[&#39;key 3&#39;] = {&#39;type&#39;: &#39;object&#39;,
+        ...               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                              &#39;key 2&#39;: True}}
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t[42] = {&#39;const&#39;: &#39;int key&#39;}
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t[&#39;key&#39;] = &#39;schema&#39;  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+        &#34;&#34;&#34;
+        if not isinstance(key, str):
+            raise TypeError(f&#34;&#39;{key}&#39; is not a valid key in MessageTemplate&#34;
+                            &#34; (not a string).&#34;)
+        try:
+            jsonschema.Draft7Validator.check_schema(value)
+            # Draft7Validator is hardcoded, because _LATEST_VERSION is
+            # non-public in jsonschema and we also perhaps do not want to
+            # upgrade automatically.
+        except jsonschema.exceptions.SchemaError:
+            raise TypeError(f&#34;&#39;{value}&#39; is not a valid value in&#34;
+                            &#34; MessageTemplate (not a valid JSON schema).&#34;)
+        super().__setitem__(key, value)
+
+    def update(self, *args, **kwargs) -&gt; None:
+        &#34;&#34;&#34;Override update to use validity checks.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t.update({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...           &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...           &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                     &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True}}})
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t.update({42: {&#39;const&#39;: &#39;int key&#39;}})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t.update({&#39;key&#39;: &#39;schema&#39;})  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+
+        This is also used in __init__:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                                &#39;properties&#39;: {
+        ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True}}})
+        &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+        {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+         &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}}
+        &gt;&gt;&gt; t = MessageTemplate({42: {&#39;const&#39;: &#39;int key&#39;}})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: &#39;schema&#39;})
+        ... # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+        &#34;&#34;&#34;
+        if args:
+            if len(args) &gt; 1:
+                raise TypeError(&#34;update expected at most 1 argument,&#34;
+                                f&#34; got {len(args)}&#34;)
+            other = dict(args[0])
+            for key in other:
+                self[key] = other[key]
+        for key in kwargs:
+            self[key] = kwargs[key]
+
+    def setdefault(self, key: str, value: JSONSchema = None) -&gt; JSONSchema:
+        &#34;&#34;&#34;Override setdefault to use validity checks.
+
+        &gt;&gt;&gt; t = MessageTemplate()
+        &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, {&#39;const&#39;: &#39;value&#39;})
+        {&#39;const&#39;: &#39;value&#39;}
+        &gt;&gt;&gt; t.setdefault(&#39;key 2&#39;, {&#39;type&#39;: &#39;string&#39;})
+        {&#39;type&#39;: &#39;string&#39;}
+        &gt;&gt;&gt; t.setdefault(&#39;key 3&#39;, {&#39;type&#39;: &#39;object&#39;,
+        ...                        &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                       &#39;key 2&#39;: True}})
+        ... # doctest: +NORMALIZE_WHITESPACE
+        {&#39;type&#39;: &#39;object&#39;,
+                   &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                  &#39;key 2&#39;: True}}
+        &gt;&gt;&gt; t.setdefault(42, {&#39;const&#39;: &#39;int key&#39;})
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+        &gt;&gt;&gt; t.setdefault(&#39;key&#39;, &#39;schema&#39;)  # doctest: +NORMALIZE_WHITESPACE
+        Traceback (most recent call last):
+          ...
+        TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+        (not a valid JSON schema).
+
+        But __setitem__ is not called if the key is already present:
+        &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, &#39;schema&#39;)
+        {&#39;const&#39;: &#39;value&#39;}
+        &#34;&#34;&#34;
+        if key not in self:
+            if value is not None:
+                self[key] = value
+            else:
+                self[key] = True
+        return self[key]
+
+    def check(self, message: Message) -&gt; bool:
+        &#34;&#34;&#34;Check message against this template.
+
+        Constant values have to match exactly:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;other value&#39;}))
+        False
+
+        But for integers, floats with the same value are also valid:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: 42}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+        True
+
+        Type integer is valid for floats with zero fractional part, but
+        not by floats with non-zero fractional part:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;integer&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+        False
+
+        Type number is valid for arbitrary ints or floats:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;number&#39;}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+        True
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+        True
+
+        All keys in template have to be present in message:
+        &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+        ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+        ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+        ...                                &#39;properties&#39;: {
+        ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+        ...                                    &#39;key 2&#39;: True,
+        ...                                    &#39;key 3&#39;: False}}})
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;}))
+        False
+
+        But for nested objects their properties do not necessarily have
+        to be present:
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 1&#39;: 42}}))
+        True
+
+        Schema True matches everything (even None):
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 2&#39;: None}}))
+        True
+
+        Schema False matches nothing:
+        &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+        ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+        ...                  &#39;key 3&#39;: {&#39;key 3&#39;: True}}))
+        False
+
+        Message is valid for the constant template created from it:
+        &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+        ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+        &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+        &gt;&gt;&gt; t.check(m)
+        True
+        &#34;&#34;&#34;
+        for key in self:
+            if key not in message:
+                return False
+            else:
+                validator = jsonschema.Draft7Validator(self[key])
+                for error in validator.iter_errors(message[key]):
+                    return False
+        return True</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>builtins.dict</li>
+<li>typing.Generic</li>
+</ul>
+<h3>Static methods</h3>
+<dl>
+<dt id="controlpi.messagebus.MessageTemplate.from_message"><code class="name flex">
+<span>def <span class="ident">from_message</span></span>(<span>message: <a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>) ‑> <a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a></span>
+</code></dt>
+<dd>
+<div class="desc"><p>Create template from message.</p>
+<p>Template witch constant schemas is created from message:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example Sender', {'key': 'value'})
+&gt;&gt;&gt; t = MessageTemplate.from_message(m)
+&gt;&gt;&gt; print(t)
+{'sender': {'const': 'Example Sender'}, 'key': {'const': 'value'}}
+&gt;&gt;&gt; m = Message('Example Sender', {'dict': {'int': 42, 'float': 42.42},
+...                                'list': [None, True, 'string']})
+&gt;&gt;&gt; t = MessageTemplate.from_message(m)
+&gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+{'sender': {'const': 'Example Sender'},
+ 'dict': {'type': 'object',
+          'properties': {'int': {'const': 42},
+                         'float': {'const': 42.42}}},
+ 'list': {'type': 'array',
+          'items': [{'const': None},
+                    {'const': True},
+                    {'const': 'string'}]}}
+</code></pre>
+<p>This is especially useful for clients that send certain fully
+predefined messages, where the message is given in the configuration
+and the template for the registration can be constructed by this
+method.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">@staticmethod
+def from_message(message: Message) -&gt; &#39;MessageTemplate&#39;:
+    &#34;&#34;&#34;Create template from message.
+
+    Template witch constant schemas is created from message:
+    &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;})
+    &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+    &gt;&gt;&gt; print(t)
+    {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;}, &#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}}
+    &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+    ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+    &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+    &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+    {&#39;sender&#39;: {&#39;const&#39;: &#39;Example Sender&#39;},
+     &#39;dict&#39;: {&#39;type&#39;: &#39;object&#39;,
+              &#39;properties&#39;: {&#39;int&#39;: {&#39;const&#39;: 42},
+                             &#39;float&#39;: {&#39;const&#39;: 42.42}}},
+     &#39;list&#39;: {&#39;type&#39;: &#39;array&#39;,
+              &#39;items&#39;: [{&#39;const&#39;: None},
+                        {&#39;const&#39;: True},
+                        {&#39;const&#39;: &#39;string&#39;}]}}
+
+    This is especially useful for clients that send certain fully
+    predefined messages, where the message is given in the configuration
+    and the template for the registration can be constructed by this
+    method.
+    &#34;&#34;&#34;
+    def schema_from_value(value: MessageValue) -&gt; JSONSchema:
+        schema: JSONSchema = False
+        if value is None:
+            schema = {&#39;const&#39;: None}
+        elif (isinstance(value, str) or isinstance(value, int) or
+                isinstance(value, float) or isinstance(value, bool)):
+            schema = {&#39;const&#39;: value}
+        elif isinstance(value, dict):
+            properties = {}
+            for inner_key in value:
+                inner_value: Message = value[inner_key]
+                properties[inner_key] = schema_from_value(inner_value)
+            schema = {&#39;type&#39;: &#39;object&#39;,
+                      &#39;properties&#39;: properties}
+        elif isinstance(value, list):
+            schema = {&#39;type&#39;: &#39;array&#39;,
+                      &#39;items&#39;: [schema_from_value(element)
+                                for element in value]}
+        return schema
+    template = MessageTemplate()
+    for key in message:
+        template[key] = schema_from_value(message[key])
+    return template</code></pre>
+</details>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi.messagebus.MessageTemplate.update"><code class="name flex">
+<span>def <span class="ident">update</span></span>(<span>self, *args, **kwargs) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Override update to use validity checks.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate()
+&gt;&gt;&gt; t.update({'key 1': {'const': 'value'},
+...           'key 2': {'type': 'string'},
+...           'key 3': {'type': 'object',
+...                     'properties': {'key 1': {'type': 'number'},
+...                                    'key 2': True}}})
+&gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+{'key 1': {'const': 'value'}, 'key 2': {'type': 'string'},
+ 'key 3': {'type': 'object',
+           'properties': {'key 1': {'type': 'number'},
+                          'key 2': True}}}
+&gt;&gt;&gt; t.update({42: {'const': 'int key'}})
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in MessageTemplate (not a string).
+&gt;&gt;&gt; t.update({'key': 'schema'})  # doctest: +NORMALIZE_WHITESPACE
+Traceback (most recent call last):
+  ...
+TypeError: 'schema' is not a valid value in MessageTemplate
+(not a valid JSON schema).
+</code></pre>
+<p>This is also used in <strong>init</strong>:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key 1': {'const': 'value'},
+...                      'key 2': {'type': 'string'},
+...                      'key 3': {'type': 'object',
+...                                'properties': {
+...                                    'key 1': {'type': 'number'},
+...                                    'key 2': True}}})
+&gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+{'key 1': {'const': 'value'}, 'key 2': {'type': 'string'},
+ 'key 3': {'type': 'object',
+           'properties': {'key 1': {'type': 'number'},
+                          'key 2': True}}}
+&gt;&gt;&gt; t = MessageTemplate({42: {'const': 'int key'}})
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in MessageTemplate (not a string).
+&gt;&gt;&gt; t = MessageTemplate({'key': 'schema'})
+... # doctest: +NORMALIZE_WHITESPACE
+Traceback (most recent call last):
+  ...
+TypeError: 'schema' is not a valid value in MessageTemplate
+(not a valid JSON schema).
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def update(self, *args, **kwargs) -&gt; None:
+    &#34;&#34;&#34;Override update to use validity checks.
+
+    &gt;&gt;&gt; t = MessageTemplate()
+    &gt;&gt;&gt; t.update({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+    ...           &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+    ...           &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+    ...                     &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                                    &#39;key 2&#39;: True}}})
+    &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+    {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+     &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                              &#39;key 2&#39;: True}}}
+    &gt;&gt;&gt; t.update({42: {&#39;const&#39;: &#39;int key&#39;}})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+    &gt;&gt;&gt; t.update({&#39;key&#39;: &#39;schema&#39;})  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+    (not a valid JSON schema).
+
+    This is also used in __init__:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+    ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+    ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+    ...                                &#39;properties&#39;: {
+    ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                                    &#39;key 2&#39;: True}}})
+    &gt;&gt;&gt; print(t)  # doctest: +NORMALIZE_WHITESPACE
+    {&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;}, &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+     &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                              &#39;key 2&#39;: True}}}
+    &gt;&gt;&gt; t = MessageTemplate({42: {&#39;const&#39;: &#39;int key&#39;}})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: &#39;schema&#39;})
+    ... # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+    (not a valid JSON schema).
+    &#34;&#34;&#34;
+    if args:
+        if len(args) &gt; 1:
+            raise TypeError(&#34;update expected at most 1 argument,&#34;
+                            f&#34; got {len(args)}&#34;)
+        other = dict(args[0])
+        for key in other:
+            self[key] = other[key]
+    for key in kwargs:
+        self[key] = kwargs[key]</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplate.setdefault"><code class="name flex">
+<span>def <span class="ident">setdefault</span></span>(<span>self, key: str, value: Union[bool, Dict[str, Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]]] = None) ‑> Union[bool, Dict[str, Union[NoneType, str, int, float, bool, Dict[str, Any], List[Any]]]]</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Override setdefault to use validity checks.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate()
+&gt;&gt;&gt; t.setdefault('key 1', {'const': 'value'})
+{'const': 'value'}
+&gt;&gt;&gt; t.setdefault('key 2', {'type': 'string'})
+{'type': 'string'}
+&gt;&gt;&gt; t.setdefault('key 3', {'type': 'object',
+...                        'properties': {'key 1': {'type': 'number'},
+...                                       'key 2': True}})
+... # doctest: +NORMALIZE_WHITESPACE
+{'type': 'object',
+           'properties': {'key 1': {'type': 'number'},
+                          'key 2': True}}
+&gt;&gt;&gt; t.setdefault(42, {'const': 'int key'})
+Traceback (most recent call last):
+  ...
+TypeError: '42' is not a valid key in MessageTemplate (not a string).
+&gt;&gt;&gt; t.setdefault('key', 'schema')  # doctest: +NORMALIZE_WHITESPACE
+Traceback (most recent call last):
+  ...
+TypeError: 'schema' is not a valid value in MessageTemplate
+(not a valid JSON schema).
+</code></pre>
+<p>But <strong>setitem</strong> is not called if the key is already present:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t.setdefault('key 1', 'schema')
+{'const': 'value'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def setdefault(self, key: str, value: JSONSchema = None) -&gt; JSONSchema:
+    &#34;&#34;&#34;Override setdefault to use validity checks.
+
+    &gt;&gt;&gt; t = MessageTemplate()
+    &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, {&#39;const&#39;: &#39;value&#39;})
+    {&#39;const&#39;: &#39;value&#39;}
+    &gt;&gt;&gt; t.setdefault(&#39;key 2&#39;, {&#39;type&#39;: &#39;string&#39;})
+    {&#39;type&#39;: &#39;string&#39;}
+    &gt;&gt;&gt; t.setdefault(&#39;key 3&#39;, {&#39;type&#39;: &#39;object&#39;,
+    ...                        &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                                       &#39;key 2&#39;: True}})
+    ... # doctest: +NORMALIZE_WHITESPACE
+    {&#39;type&#39;: &#39;object&#39;,
+               &#39;properties&#39;: {&#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+                              &#39;key 2&#39;: True}}
+    &gt;&gt;&gt; t.setdefault(42, {&#39;const&#39;: &#39;int key&#39;})
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;42&#39; is not a valid key in MessageTemplate (not a string).
+    &gt;&gt;&gt; t.setdefault(&#39;key&#39;, &#39;schema&#39;)  # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+      ...
+    TypeError: &#39;schema&#39; is not a valid value in MessageTemplate
+    (not a valid JSON schema).
+
+    But __setitem__ is not called if the key is already present:
+    &gt;&gt;&gt; t.setdefault(&#39;key 1&#39;, &#39;schema&#39;)
+    {&#39;const&#39;: &#39;value&#39;}
+    &#34;&#34;&#34;
+    if key not in self:
+        if value is not None:
+            self[key] = value
+        else:
+            self[key] = True
+    return self[key]</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplate.check"><code class="name flex">
+<span>def <span class="ident">check</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>) ‑> bool</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Check message against this template.</p>
+<p>Constant values have to match exactly:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key': {'const': 'value'}})
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 'value'}))
+True
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 'other value'}))
+False
+</code></pre>
+<p>But for integers, floats with the same value are also valid:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key': {'const': 42}})
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42}))
+True
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42.0}))
+True
+</code></pre>
+<p>Type integer is valid for floats with zero fractional part, but
+not by floats with non-zero fractional part:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key': {'type': 'integer'}})
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42}))
+True
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42.0}))
+True
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42.42}))
+False
+</code></pre>
+<p>Type number is valid for arbitrary ints or floats:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key': {'type': 'number'}})
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42}))
+True
+&gt;&gt;&gt; t.check(Message('Example Sender', {'key': 42.42}))
+True
+</code></pre>
+<p>All keys in template have to be present in message:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t = MessageTemplate({'key 1': {'const': 'value'},
+...                      'key 2': {'type': 'string'},
+...                      'key 3': {'type': 'object',
+...                                'properties': {
+...                                    'key 1': {'type': 'number'},
+...                                    'key 2': True,
+...                                    'key 3': False}}})
+&gt;&gt;&gt; t.check(Message('Example Sender',
+...                 {'key 1': 'value', 'key 2': 'some string'}))
+False
+</code></pre>
+<p>But for nested objects their properties do not necessarily have
+to be present:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t.check(Message('Example Sender',
+...                 {'key 1': 'value', 'key 2': 'some string',
+...                  'key 3': {'key 1': 42}}))
+True
+</code></pre>
+<p>Schema True matches everything (even None):</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t.check(Message('Example Sender',
+...                 {'key 1': 'value', 'key 2': 'some string',
+...                  'key 3': {'key 2': None}}))
+True
+</code></pre>
+<p>Schema False matches nothing:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; t.check(Message('Example Sender',
+...                 {'key 1': 'value', 'key 2': 'some string',
+...                  'key 3': {'key 3': True}}))
+False
+</code></pre>
+<p>Message is valid for the constant template created from it:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; m = Message('Example Sender', {'dict': {'int': 42, 'float': 42.42},
+...                                'list': [None, True, 'string']})
+&gt;&gt;&gt; t = MessageTemplate.from_message(m)
+&gt;&gt;&gt; t.check(m)
+True
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def check(self, message: Message) -&gt; bool:
+    &#34;&#34;&#34;Check message against this template.
+
+    Constant values have to match exactly:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: &#39;value&#39;}})
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;value&#39;}))
+    True
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: &#39;other value&#39;}))
+    False
+
+    But for integers, floats with the same value are also valid:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;const&#39;: 42}})
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+    True
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+    True
+
+    Type integer is valid for floats with zero fractional part, but
+    not by floats with non-zero fractional part:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;integer&#39;}})
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+    True
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.0}))
+    True
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+    False
+
+    Type number is valid for arbitrary ints or floats:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key&#39;: {&#39;type&#39;: &#39;number&#39;}})
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42}))
+    True
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;, {&#39;key&#39;: 42.42}))
+    True
+
+    All keys in template have to be present in message:
+    &gt;&gt;&gt; t = MessageTemplate({&#39;key 1&#39;: {&#39;const&#39;: &#39;value&#39;},
+    ...                      &#39;key 2&#39;: {&#39;type&#39;: &#39;string&#39;},
+    ...                      &#39;key 3&#39;: {&#39;type&#39;: &#39;object&#39;,
+    ...                                &#39;properties&#39;: {
+    ...                                    &#39;key 1&#39;: {&#39;type&#39;: &#39;number&#39;},
+    ...                                    &#39;key 2&#39;: True,
+    ...                                    &#39;key 3&#39;: False}}})
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;}))
+    False
+
+    But for nested objects their properties do not necessarily have
+    to be present:
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+    ...                  &#39;key 3&#39;: {&#39;key 1&#39;: 42}}))
+    True
+
+    Schema True matches everything (even None):
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+    ...                  &#39;key 3&#39;: {&#39;key 2&#39;: None}}))
+    True
+
+    Schema False matches nothing:
+    &gt;&gt;&gt; t.check(Message(&#39;Example Sender&#39;,
+    ...                 {&#39;key 1&#39;: &#39;value&#39;, &#39;key 2&#39;: &#39;some string&#39;,
+    ...                  &#39;key 3&#39;: {&#39;key 3&#39;: True}}))
+    False
+
+    Message is valid for the constant template created from it:
+    &gt;&gt;&gt; m = Message(&#39;Example Sender&#39;, {&#39;dict&#39;: {&#39;int&#39;: 42, &#39;float&#39;: 42.42},
+    ...                                &#39;list&#39;: [None, True, &#39;string&#39;]})
+    &gt;&gt;&gt; t = MessageTemplate.from_message(m)
+    &gt;&gt;&gt; t.check(m)
+    True
+    &#34;&#34;&#34;
+    for key in self:
+        if key not in message:
+            return False
+        else:
+            validator = jsonschema.Draft7Validator(self[key])
+            for error in validator.iter_errors(message[key]):
+                return False
+    return True</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplateRegistry"><code class="flex name class">
+<span>class <span class="ident">MessageTemplateRegistry</span></span>
+</code></dt>
+<dd>
+<div class="desc"><p>Manage a collection of message templates with registered clients.</p>
+<p>A new MessageTemplateRegistry is created by:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+</code></pre>
+<p>Client names (strings) can be registered for message templates, which
+are mappings from keys to JSON schemas:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'C 1')
+</code></pre>
+<p>The check function checks if the templates registered for a client
+match a given message:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 1', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: True
+{'k1': 'v1', 'k2': 2}: True
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 2}: False
+</code></pre>
+<p>Clients can be registered for values validating against arbitrary JSON
+schemas, e.g. all values of a certain type:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'type': 'string'}}, 'C 2')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 2', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: False
+{'k1': 'v1', 'k2': 2}: False
+{'k1': 'v2', 'k2': 'v1'}: True
+{'k1': 'v2', 'k2': 2}: False
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'type': 'integer'}}, 'C 3')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 3', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: False
+{'k1': 'v1', 'k2': 2}: False
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 2}: True
+</code></pre>
+<p>The order of key-value pairs does not have to match the order in the
+messages and keys can be left out:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({'k2': {'const': 2}}, 'C 4')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 4', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: False
+{'k1': 'v1', 'k2': 2}: True
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 2}: True
+</code></pre>
+<p>A registration for an empty template matches all messages:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({}, 'C 5')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 5', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: True
+{'k1': 'v1', 'k2': 2}: True
+{'k1': 'v2', 'k2': 'v1'}: True
+{'k1': 'v2', 'k2': 2}: True
+</code></pre>
+<p>A client can be registered for multiple templates:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'C 6')
+&gt;&gt;&gt; r.insert({'k2': {'const': 'v1'}}, 'C 6')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 6', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: True
+{'k1': 'v1', 'k2': 2}: True
+{'k1': 'v2', 'k2': 'v1'}: True
+{'k1': 'v2', 'k2': 2}: False
+</code></pre>
+<p>Clients can be deregistered again (the result is False if the registry
+is empty after the deletion):</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'C 7')
+&gt;&gt;&gt; r.delete('C 7')
+True
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.check('C 7', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: False
+{'k1': 'v1', 'k2': 2}: False
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 2}: False
+</code></pre>
+<p>The get function returns all clients with registered templates matching
+a given message:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 2},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 2}]:
+...     print(f&quot;{m}: {r.get(m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: ['C 5', 'C 1', 'C 6']
+{'k1': 'v1', 'k2': 2}: ['C 5', 'C 1', 'C 6', 'C 4']
+{'k1': 'v2', 'k2': 'v1'}: ['C 5', 'C 2', 'C 6']
+{'k1': 'v2', 'k2': 2}: ['C 5', 'C 3', 'C 4']
+</code></pre>
+<p>The get_templates function returns all templates for a given client:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; for c in ['C 1', 'C 2', 'C 3', 'C 4', 'C 5', 'C 6']:
+...     print(f&quot;{c}: {r.get_templates(c)}&quot;)
+C 1: [{'k1': {'const': 'v1'}}]
+C 2: [{'k1': {'const': 'v2'}, 'k2': {'type': 'string'}}]
+C 3: [{'k1': {'const': 'v2'}, 'k2': {'type': 'integer'}}]
+C 4: [{'k2': {'const': 2}}]
+C 5: [{}]
+C 6: [{'k1': {'const': 'v1'}}, {'k2': {'const': 'v1'}}]
+</code></pre>
+<p>Initialise an empty registry.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class MessageTemplateRegistry:
+    &#34;&#34;&#34;Manage a collection of message templates with registered clients.
+
+    A new MessageTemplateRegistry is created by:
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+
+    Client names (strings) can be registered for message templates, which
+    are mappings from keys to JSON schemas:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+
+    The check function checks if the templates registered for a client
+    match a given message:
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 1&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    Clients can be registered for values validating against arbitrary JSON
+    schemas, e.g. all values of a certain type:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}, &#39;C 2&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 2&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}, &#39;C 3&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 3&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    The order of key-value pairs does not have to match the order in the
+    messages and keys can be left out:
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: 2}}, &#39;C 4&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 4&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    A registration for an empty template matches all messages:
+    &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 5&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: True
+
+    A client can be registered for multiple templates:
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 6&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 6&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 6&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    Clients can be deregistered again (the result is False if the registry
+    is empty after the deletion):
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 7&#39;)
+    &gt;&gt;&gt; r.delete(&#39;C 7&#39;)
+    True
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.check(&#39;C 7&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: False
+
+    The get function returns all clients with registered templates matching
+    a given message:
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}]:
+    ...     print(f&#34;{m}: {r.get(m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;C 5&#39;, &#39;C 1&#39;, &#39;C 6&#39;]
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: 2}: [&#39;C 5&#39;, &#39;C 1&#39;, &#39;C 6&#39;, &#39;C 4&#39;]
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;C 5&#39;, &#39;C 2&#39;, &#39;C 6&#39;]
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: 2}: [&#39;C 5&#39;, &#39;C 3&#39;, &#39;C 4&#39;]
+
+    The get_templates function returns all templates for a given client:
+    &gt;&gt;&gt; for c in [&#39;C 1&#39;, &#39;C 2&#39;, &#39;C 3&#39;, &#39;C 4&#39;, &#39;C 5&#39;, &#39;C 6&#39;]:
+    ...     print(f&#34;{c}: {r.get_templates(c)}&#34;)
+    C 1: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    C 2: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}]
+    C 3: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}]
+    C 4: [{&#39;k2&#39;: {&#39;const&#39;: 2}}]
+    C 5: [{}]
+    C 6: [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, {&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    &#34;&#34;&#34;
+
+    def __init__(self) -&gt; None:
+        &#34;&#34;&#34;Initialise an empty registry.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &#34;&#34;&#34;
+        self._clients: List[str] = []
+        self._children: Dict[str, Dict[str, MessageTemplateRegistry]] = {}
+
+    def insert(self, template: MessageTemplate, client: str) -&gt; None:
+        &#34;&#34;&#34;Register a client for a template.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+        &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+
+        Implementation details:
+        -----------------------
+        The tree nodes on the way to a registered object are used/created
+        in the order given in the message template, which can be used to
+        design more efficient lookups (e.g., putting rarer key-value pairs
+        earlier in the template).
+        &gt;&gt;&gt; r._clients
+        [&#39;C 5&#39;]
+        &gt;&gt;&gt; r._children.keys()
+        dict_keys([&#39;k1&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 1&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 2&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 3&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 4&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &#34;&#34;&#34;
+        if not template:
+            self._clients.append(client)
+        else:
+            key, schema = next(iter(template.items()))
+            schema_string = json.dumps(schema)
+            reduced_template = MessageTemplate({k: template[k]
+                                                for k in template
+                                                if k != key})
+            if key not in self._children:
+                self._children[key] = {}
+            if schema_string not in self._children[key]:
+                self._children[key][schema_string] = MessageTemplateRegistry()
+            self._children[key][schema_string].insert(reduced_template, client)
+
+    def delete(self, client: str) -&gt; bool:
+        &#34;&#34;&#34;Unregister a client from all templates.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+        &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+        &gt;&gt;&gt; r.delete(&#39;C 3&#39;)
+        True
+        &gt;&gt;&gt; r.delete(&#39;C 4&#39;)
+        True
+
+        Implementation details:
+        -----------------------
+        If parts of the tree become superfluous by the deletion of the
+        client, they are also completely removed to reduce the lookup
+        effort and keep the tree clean.
+        &gt;&gt;&gt; r._clients
+        [&#39;C 5&#39;]
+        &gt;&gt;&gt; r._children.keys()
+        dict_keys([&#39;k1&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+        []
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+        dict_keys([&#39;k2&#39;])
+        &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+        dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+        [&#39;C 1&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+        [&#39;C 2&#39;]
+        &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+        ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+        dict_keys([])
+        &#34;&#34;&#34;
+        self._clients = [c for c in self._clients if c != client]
+        new_children: Dict[str, Dict[str, MessageTemplateRegistry]] = {}
+        for key in self._children:
+            new_children[key] = {}
+            for schema in self._children[key]:
+                if self._children[key][schema].delete(client):
+                    new_children[key][schema] = self._children[key][schema]
+            if not new_children[key]:
+                del new_children[key]
+        self._children = new_children
+        if self._clients or self._children:
+            return True
+        return False
+
+    def check(self, client: str, message: Message) -&gt; bool:
+        &#34;&#34;&#34;Get if a client has a registered template matching a message.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.check(&#39;Client 1&#39;, m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: False
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.check(&#39;Client 2&#39;, m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+        &#34;&#34;&#34;
+        if client in self._clients:
+            return True
+        for key in self._children:
+            if key in message:
+                for schema_string in self._children[key]:
+                    schema = json.loads(schema_string)
+                    validator = jsonschema.Draft7Validator(schema)
+                    validated = True
+                    for error in validator.iter_errors(message[key]):
+                        validated = False
+                    if validated:
+                        child = self._children[key][schema_string]
+                        if child.check(client, message):
+                            return True
+        return False
+
+    def get(self, message: Message) -&gt; List[str]:
+        &#34;&#34;&#34;Get all clients registered for templates matching a message.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+        ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+        ...     print(f&#34;{m}: {r.get(m)}&#34;)
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;Client 1&#39;]
+        {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 1&#39;, &#39;Client 2&#39;]
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: []
+        {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 2&#39;]
+        &#34;&#34;&#34;
+        result = []
+        for client in self._clients:
+            if client not in result:
+                result.append(client)
+        for key in self._children:
+            if key in message:
+                for schema_string in self._children[key]:
+                    schema = json.loads(schema_string)
+                    validator = jsonschema.Draft7Validator(schema)
+                    validated = True
+                    for error in validator.iter_errors(message[key]):
+                        validated = False
+                    if validated:
+                        child = self._children[key][schema_string]
+                        for client in child.get(message):
+                            if client not in result:
+                                result.append(client)
+        return result
+
+    def get_templates(self, client: str) -&gt; List[MessageTemplate]:
+        &#34;&#34;&#34;Get all templates for a client.
+
+        &gt;&gt;&gt; r = MessageTemplateRegistry()
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 1&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+        ...           &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}, &#39;Client 2&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 2&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+        ...           &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}, &#39;Client 3&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 3&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}]
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: 2}}, &#39;Client 4&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 4&#39;)
+        [{&#39;k2&#39;: {&#39;const&#39;: 2}}]
+        &gt;&gt;&gt; r.insert({}, &#39;Client 5&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 5&#39;)
+        [{}]
+        &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+        &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+        &gt;&gt;&gt; r.get_templates(&#39;Client 6&#39;)
+        [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, {&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+        &#34;&#34;&#34;
+        result = []
+        if client in self._clients:
+            result.append(MessageTemplate())
+        for key in self._children:
+            for schema_string in self._children[key]:
+                schema = json.loads(schema_string)
+                child = self._children[key][schema_string]
+                for template in child.get_templates(client):
+                    current = MessageTemplate({key: schema})
+                    current.update(template)
+                    result.append(current)
+        return result</code></pre>
+</details>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi.messagebus.MessageTemplateRegistry.insert"><code class="name flex">
+<span>def <span class="ident">insert</span></span>(<span>self, template: <a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a>, client: str) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register a client for a template.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}, 'k2': {'const': 'v1'}}, 'C 1')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}, 'k2': {'const': 'v2'}}, 'C 2')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'const': 'v1'}}, 'C 3')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'const': 'v2'}}, 'C 4')
+&gt;&gt;&gt; r.insert({}, 'C 5')
+</code></pre>
+<h2 id="implementation-details">Implementation details:</h2>
+<p>The tree nodes on the way to a registered object are used/created
+in the order given in the message template, which can be used to
+design more efficient lookups (e.g., putting rarer key-value pairs
+earlier in the template).</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r._clients
+['C 5']
+&gt;&gt;&gt; r._children.keys()
+dict_keys(['k1'])
+&gt;&gt;&gt; r._children['k1'].keys()
+dict_keys(['{&quot;const&quot;: &quot;v1&quot;}', '{&quot;const&quot;: &quot;v2&quot;}'])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._clients
+[]
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._children.keys()
+dict_keys(['k2'])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._children['k2'].keys()
+dict_keys(['{&quot;const&quot;: &quot;v1&quot;}', '{&quot;const&quot;: &quot;v2&quot;}'])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._clients
+['C 1']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._children.keys()
+dict_keys([])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._clients
+['C 2']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._children.keys()
+dict_keys([])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']._clients
+[]
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']._children.keys()
+dict_keys(['k2'])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']._children['k2'].keys()
+dict_keys(['{&quot;const&quot;: &quot;v1&quot;}', '{&quot;const&quot;: &quot;v2&quot;}'])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._clients
+['C 3']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._children.keys()
+dict_keys([])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._clients
+['C 4']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v2&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._children.keys()
+dict_keys([])
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def insert(self, template: MessageTemplate, client: str) -&gt; None:
+    &#34;&#34;&#34;Register a client for a template.
+
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+    &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+
+    Implementation details:
+    -----------------------
+    The tree nodes on the way to a registered object are used/created
+    in the order given in the message template, which can be used to
+    design more efficient lookups (e.g., putting rarer key-value pairs
+    earlier in the template).
+    &gt;&gt;&gt; r._clients
+    [&#39;C 5&#39;]
+    &gt;&gt;&gt; r._children.keys()
+    dict_keys([&#39;k1&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+    dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+    []
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+    dict_keys([&#39;k2&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+    dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+    [&#39;C 1&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+    [&#39;C 2&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._clients
+    []
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children.keys()
+    dict_keys([&#39;k2&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]._children[&#39;k2&#39;].keys()
+    dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+    [&#39;C 3&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+    [&#39;C 4&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &#34;&#34;&#34;
+    if not template:
+        self._clients.append(client)
+    else:
+        key, schema = next(iter(template.items()))
+        schema_string = json.dumps(schema)
+        reduced_template = MessageTemplate({k: template[k]
+                                            for k in template
+                                            if k != key})
+        if key not in self._children:
+            self._children[key] = {}
+        if schema_string not in self._children[key]:
+            self._children[key][schema_string] = MessageTemplateRegistry()
+        self._children[key][schema_string].insert(reduced_template, client)</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplateRegistry.delete"><code class="name flex">
+<span>def <span class="ident">delete</span></span>(<span>self, client: str) ‑> bool</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Unregister a client from all templates.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}, 'k2': {'const': 'v1'}}, 'C 1')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}, 'k2': {'const': 'v2'}}, 'C 2')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'const': 'v1'}}, 'C 3')
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'}, 'k2': {'const': 'v2'}}, 'C 4')
+&gt;&gt;&gt; r.insert({}, 'C 5')
+&gt;&gt;&gt; r.delete('C 3')
+True
+&gt;&gt;&gt; r.delete('C 4')
+True
+</code></pre>
+<h2 id="implementation-details">Implementation details:</h2>
+<p>If parts of the tree become superfluous by the deletion of the
+client, they are also completely removed to reduce the lookup
+effort and keep the tree clean.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r._clients
+['C 5']
+&gt;&gt;&gt; r._children.keys()
+dict_keys(['k1'])
+&gt;&gt;&gt; r._children['k1'].keys()
+dict_keys(['{&quot;const&quot;: &quot;v1&quot;}'])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._clients
+[]
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._children.keys()
+dict_keys(['k2'])
+&gt;&gt;&gt; r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']._children['k2'].keys()
+dict_keys(['{&quot;const&quot;: &quot;v1&quot;}', '{&quot;const&quot;: &quot;v2&quot;}'])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._clients
+['C 1']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v1&quot;}'])._children.keys()
+dict_keys([])
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._clients
+['C 2']
+&gt;&gt;&gt; (r._children['k1']['{&quot;const&quot;: &quot;v1&quot;}']
+...   ._children['k2']['{&quot;const&quot;: &quot;v2&quot;}'])._children.keys()
+dict_keys([])
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def delete(self, client: str) -&gt; bool:
+    &#34;&#34;&#34;Unregister a client from all templates.
+
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 1&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 2&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;C 3&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;C 4&#39;)
+    &gt;&gt;&gt; r.insert({}, &#39;C 5&#39;)
+    &gt;&gt;&gt; r.delete(&#39;C 3&#39;)
+    True
+    &gt;&gt;&gt; r.delete(&#39;C 4&#39;)
+    True
+
+    Implementation details:
+    -----------------------
+    If parts of the tree become superfluous by the deletion of the
+    client, they are also completely removed to reduce the lookup
+    effort and keep the tree clean.
+    &gt;&gt;&gt; r._clients
+    [&#39;C 5&#39;]
+    &gt;&gt;&gt; r._children.keys()
+    dict_keys([&#39;k1&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;].keys()
+    dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._clients
+    []
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children.keys()
+    dict_keys([&#39;k2&#39;])
+    &gt;&gt;&gt; r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]._children[&#39;k2&#39;].keys()
+    dict_keys([&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;, &#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._clients
+    [&#39;C 1&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._clients
+    [&#39;C 2&#39;]
+    &gt;&gt;&gt; (r._children[&#39;k1&#39;][&#39;{&#34;const&#34;: &#34;v1&#34;}&#39;]
+    ...   ._children[&#39;k2&#39;][&#39;{&#34;const&#34;: &#34;v2&#34;}&#39;])._children.keys()
+    dict_keys([])
+    &#34;&#34;&#34;
+    self._clients = [c for c in self._clients if c != client]
+    new_children: Dict[str, Dict[str, MessageTemplateRegistry]] = {}
+    for key in self._children:
+        new_children[key] = {}
+        for schema in self._children[key]:
+            if self._children[key][schema].delete(client):
+                new_children[key][schema] = self._children[key][schema]
+        if not new_children[key]:
+            del new_children[key]
+    self._children = new_children
+    if self._clients or self._children:
+        return True
+    return False</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplateRegistry.check"><code class="name flex">
+<span>def <span class="ident">check</span></span>(<span>self, client: str, message: <a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>) ‑> bool</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Get if a client has a registered template matching a message.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'Client 1')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 'v2'},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 'v2'}]:
+...     print(f&quot;{m}: {r.check('Client 1', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: True
+{'k1': 'v1', 'k2': 'v2'}: True
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 'v2'}: False
+&gt;&gt;&gt; r.insert({'k2': {'const': 'v2'}}, 'Client 2')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 'v2'},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 'v2'}]:
+...     print(f&quot;{m}: {r.check('Client 2', m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: False
+{'k1': 'v1', 'k2': 'v2'}: True
+{'k1': 'v2', 'k2': 'v1'}: False
+{'k1': 'v2', 'k2': 'v2'}: True
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def check(self, client: str, message: Message) -&gt; bool:
+    &#34;&#34;&#34;Get if a client has a registered template matching a message.
+
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+    ...     print(f&#34;{m}: {r.check(&#39;Client 1&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: True
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: False
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+    ...     print(f&#34;{m}: {r.check(&#39;Client 2&#39;, m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: False
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: True
+    &#34;&#34;&#34;
+    if client in self._clients:
+        return True
+    for key in self._children:
+        if key in message:
+            for schema_string in self._children[key]:
+                schema = json.loads(schema_string)
+                validator = jsonschema.Draft7Validator(schema)
+                validated = True
+                for error in validator.iter_errors(message[key]):
+                    validated = False
+                if validated:
+                    child = self._children[key][schema_string]
+                    if child.check(client, message):
+                        return True
+    return False</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplateRegistry.get"><code class="name flex">
+<span>def <span class="ident">get</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>) ‑> List[str]</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Get all clients registered for templates matching a message.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'Client 1')
+&gt;&gt;&gt; r.insert({'k2': {'const': 'v2'}}, 'Client 2')
+&gt;&gt;&gt; for m in [{'k1': 'v1', 'k2': 'v1'}, {'k1': 'v1', 'k2': 'v2'},
+...           {'k1': 'v2', 'k2': 'v1'}, {'k1': 'v2', 'k2': 'v2'}]:
+...     print(f&quot;{m}: {r.get(m)}&quot;)
+{'k1': 'v1', 'k2': 'v1'}: ['Client 1']
+{'k1': 'v1', 'k2': 'v2'}: ['Client 1', 'Client 2']
+{'k1': 'v2', 'k2': 'v1'}: []
+{'k1': 'v2', 'k2': 'v2'}: ['Client 2']
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def get(self, message: Message) -&gt; List[str]:
+    &#34;&#34;&#34;Get all clients registered for templates matching a message.
+
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v2&#39;}}, &#39;Client 2&#39;)
+    &gt;&gt;&gt; for m in [{&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;},
+    ...           {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}, {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}]:
+    ...     print(f&#34;{m}: {r.get(m)}&#34;)
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v1&#39;}: [&#39;Client 1&#39;]
+    {&#39;k1&#39;: &#39;v1&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 1&#39;, &#39;Client 2&#39;]
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v1&#39;}: []
+    {&#39;k1&#39;: &#39;v2&#39;, &#39;k2&#39;: &#39;v2&#39;}: [&#39;Client 2&#39;]
+    &#34;&#34;&#34;
+    result = []
+    for client in self._clients:
+        if client not in result:
+            result.append(client)
+    for key in self._children:
+        if key in message:
+            for schema_string in self._children[key]:
+                schema = json.loads(schema_string)
+                validator = jsonschema.Draft7Validator(schema)
+                validated = True
+                for error in validator.iter_errors(message[key]):
+                    validated = False
+                if validated:
+                    child = self._children[key][schema_string]
+                    for client in child.get(message):
+                        if client not in result:
+                            result.append(client)
+    return result</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageTemplateRegistry.get_templates"><code class="name flex">
+<span>def <span class="ident">get_templates</span></span>(<span>self, client: str) ‑> List[<a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a>]</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Get all templates for a client.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; r = MessageTemplateRegistry()
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'Client 1')
+&gt;&gt;&gt; r.get_templates('Client 1')
+[{'k1': {'const': 'v1'}}]
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'},
+...           'k2': {'type': 'string'}}, 'Client 2')
+&gt;&gt;&gt; r.get_templates('Client 2')
+[{'k1': {'const': 'v2'}, 'k2': {'type': 'string'}}]
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v2'},
+...           'k2': {'type': 'integer'}}, 'Client 3')
+&gt;&gt;&gt; r.get_templates('Client 3')
+[{'k1': {'const': 'v2'}, 'k2': {'type': 'integer'}}]
+&gt;&gt;&gt; r.insert({'k2': {'const': 2}}, 'Client 4')
+&gt;&gt;&gt; r.get_templates('Client 4')
+[{'k2': {'const': 2}}]
+&gt;&gt;&gt; r.insert({}, 'Client 5')
+&gt;&gt;&gt; r.get_templates('Client 5')
+[{}]
+&gt;&gt;&gt; r.insert({'k1': {'const': 'v1'}}, 'Client 6')
+&gt;&gt;&gt; r.insert({'k2': {'const': 'v1'}}, 'Client 6')
+&gt;&gt;&gt; r.get_templates('Client 6')
+[{'k1': {'const': 'v1'}}, {'k2': {'const': 'v1'}}]
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def get_templates(self, client: str) -&gt; List[MessageTemplate]:
+    &#34;&#34;&#34;Get all templates for a client.
+
+    &gt;&gt;&gt; r = MessageTemplateRegistry()
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 1&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 1&#39;)
+    [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+    ...           &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}, &#39;Client 2&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 2&#39;)
+    [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;string&#39;}}]
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;},
+    ...           &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}, &#39;Client 3&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 3&#39;)
+    [{&#39;k1&#39;: {&#39;const&#39;: &#39;v2&#39;}, &#39;k2&#39;: {&#39;type&#39;: &#39;integer&#39;}}]
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: 2}}, &#39;Client 4&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 4&#39;)
+    [{&#39;k2&#39;: {&#39;const&#39;: 2}}]
+    &gt;&gt;&gt; r.insert({}, &#39;Client 5&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 5&#39;)
+    [{}]
+    &gt;&gt;&gt; r.insert({&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+    &gt;&gt;&gt; r.insert({&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}, &#39;Client 6&#39;)
+    &gt;&gt;&gt; r.get_templates(&#39;Client 6&#39;)
+    [{&#39;k1&#39;: {&#39;const&#39;: &#39;v1&#39;}}, {&#39;k2&#39;: {&#39;const&#39;: &#39;v1&#39;}}]
+    &#34;&#34;&#34;
+    result = []
+    if client in self._clients:
+        result.append(MessageTemplate())
+    for key in self._children:
+        for schema_string in self._children[key]:
+            schema = json.loads(schema_string)
+            child = self._children[key][schema_string]
+            for template in child.get_templates(client):
+                current = MessageTemplate({key: schema})
+                current.update(template)
+                result.append(current)
+    return result</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi.messagebus.BusException"><code class="flex name class">
+<span>class <span class="ident">BusException</span></span>
+<span>(</span><span>*args, **kwargs)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Raise for errors in using message bus.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class BusException(Exception):
+    &#34;&#34;&#34;Raise for errors in using message bus.&#34;&#34;&#34;</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>builtins.Exception</li>
+<li>builtins.BaseException</li>
+</ul>
+</dd>
+<dt id="controlpi.messagebus.MessageBus"><code class="flex name class">
+<span>class <span class="ident">MessageBus</span></span>
+</code></dt>
+<dd>
+<div class="desc"><p>Provide an asynchronous message bus.</p>
+<p>The bus executes asynchronous callbacks for all messages to be received
+by a client. We use a simple callback printing the message in all
+examples:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; def callback_for_receiver(receiver):
+...     print(f&quot;Creating callback for {receiver}.&quot;)
+...     async def callback(message):
+...         print(f&quot;{receiver}: {message}&quot;)
+...     return callback
+</code></pre>
+<p>Clients can be registered at the bus with a name, lists of message
+templates they want to use for sending and receiving and a callback
+function for receiving. An empty list of templates means that the
+client does not want to send or receive any messages, respectively.
+A list with an empty template means that it wants to send arbitrary
+or receive all messages, respectively:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def setup(bus):
+...     print(&quot;Setting up.&quot;)
+...     bus.register('Logger', 'Test Plugin',
+...                  [],
+...                  [{}],
+...                  callback_for_receiver('Logger'))
+...     bus.register('Client 1', 'Test Plugin',
+...                  [{'k1': {'type': 'string'}}],
+...                  [{'target': {'const': 'Client 1'}}],
+...                  callback_for_receiver('Client 1'))
+...     bus.register('Client 2', 'Test Plugin',
+...                  [{}],
+...                  [{'target': {'const': 'Client 2'}}],
+...                  callback_for_receiver('Client 2'))
+</code></pre>
+<p>The bus itself is addressed by the empty string. It sends messages for
+each registration and deregestration of a client with a key 'event' and
+a value of 'registered' or 'unregistered', a key 'client' with the
+client's name as value and for registrations also keys 'sends' and
+'receives' with all templates registered for the client for sending and
+receiving.</p>
+<p>Clients can send to the bus with the send function. Each message has to
+declare a sender. The send templates of that sender are checked for a
+template matching the message. We cannot prevent arbitrary code from
+impersonating any sender, but this should only be done in debugging or
+management situations.</p>
+<p>Messages that are intended for a specific client by convention have a
+key 'target' with the target client's name as value. Such messages are
+often commands to the client to do something, which is by convention
+indicated by a key 'command' with a value that indicates what should be
+done.</p>
+<p>The bus, for example, reacts to a message with 'target': '' and
+'command': 'get clients' by sending one message for each currently
+registered with complete information about its registered send and
+receive templates.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def send(bus):
+...     print(&quot;Sending messages.&quot;)
+...     await bus.send({'sender': 'Client 1', 'k1': 'Test'})
+...     await bus.send({'sender': 'Client 2', 'target': 'Client 1'})
+...     await bus.send({'sender': '', 'target': '',
+...                     'command': 'get clients'})
+</code></pre>
+<p>The run function executes the message bus forever. If we want to stop
+it, we have to explicitly cancel the task:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     await setup(bus)
+...     bus_task = asyncio.create_task(bus.run())
+...     await send(bus)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+Setting up.
+Creating callback for Logger.
+Creating callback for Client 1.
+Creating callback for Client 2.
+Sending messages.
+Logger: {'sender': '', 'event': 'registered',
+         'client': 'Logger', 'plugin': 'Test Plugin',
+         'sends': [], 'receives': [{}]}
+Logger: {'sender': '', 'event': 'registered',
+         'client': 'Client 1', 'plugin': 'Test Plugin',
+         'sends': [{'k1': {'type': 'string'}}],
+         'receives': [{'target': {'const': 'Client 1'}}]}
+Logger: {'sender': '', 'event': 'registered',
+         'client': 'Client 2', 'plugin': 'Test Plugin',
+         'sends': [{}], 'receives': [{'target': {'const': 'Client 2'}}]}
+Logger: {'sender': 'Client 1', 'k1': 'Test'}
+Logger: {'sender': 'Client 2', 'target': 'Client 1'}
+Client 1: {'sender': 'Client 2', 'target': 'Client 1'}
+Logger: {'sender': '', 'target': '', 'command': 'get clients'}
+Logger: {'sender': '', 'client': 'Logger', 'plugin': 'Test Plugin',
+         'sends': [], 'receives': [{}]}
+Logger: {'sender': '', 'client': 'Client 1', 'plugin': 'Test Plugin',
+         'sends': [{'k1': {'type': 'string'}}],
+         'receives': [{'target': {'const': 'Client 1'}}]}
+Logger: {'sender': '', 'client': 'Client 2', 'plugin': 'Test Plugin',
+         'sends': [{}], 'receives': [{'target': {'const': 'Client 2'}}]}
+</code></pre>
+<p>Initialise a new bus without clients.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+&gt;&gt;&gt; asyncio.run(main())
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class MessageBus:
+    &#34;&#34;&#34;Provide an asynchronous message bus.
+
+    The bus executes asynchronous callbacks for all messages to be received
+    by a client. We use a simple callback printing the message in all
+    examples:
+    &gt;&gt;&gt; def callback_for_receiver(receiver):
+    ...     print(f&#34;Creating callback for {receiver}.&#34;)
+    ...     async def callback(message):
+    ...         print(f&#34;{receiver}: {message}&#34;)
+    ...     return callback
+
+    Clients can be registered at the bus with a name, lists of message
+    templates they want to use for sending and receiving and a callback
+    function for receiving. An empty list of templates means that the
+    client does not want to send or receive any messages, respectively.
+    A list with an empty template means that it wants to send arbitrary
+    or receive all messages, respectively:
+    &gt;&gt;&gt; async def setup(bus):
+    ...     print(&#34;Setting up.&#34;)
+    ...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+    ...                  [],
+    ...                  [{}],
+    ...                  callback_for_receiver(&#39;Logger&#39;))
+    ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+    ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+    ...                  callback_for_receiver(&#39;Client 1&#39;))
+    ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+    ...                  [{}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+    ...                  callback_for_receiver(&#39;Client 2&#39;))
+
+    The bus itself is addressed by the empty string. It sends messages for
+    each registration and deregestration of a client with a key &#39;event&#39; and
+    a value of &#39;registered&#39; or &#39;unregistered&#39;, a key &#39;client&#39; with the
+    client&#39;s name as value and for registrations also keys &#39;sends&#39; and
+    &#39;receives&#39; with all templates registered for the client for sending and
+    receiving.
+
+    Clients can send to the bus with the send function. Each message has to
+    declare a sender. The send templates of that sender are checked for a
+    template matching the message. We cannot prevent arbitrary code from
+    impersonating any sender, but this should only be done in debugging or
+    management situations.
+
+    Messages that are intended for a specific client by convention have a
+    key &#39;target&#39; with the target client&#39;s name as value. Such messages are
+    often commands to the client to do something, which is by convention
+    indicated by a key &#39;command&#39; with a value that indicates what should be
+    done.
+
+    The bus, for example, reacts to a message with &#39;target&#39;: &#39;&#39; and
+    &#39;command&#39;: &#39;get clients&#39; by sending one message for each currently
+    registered with complete information about its registered send and
+    receive templates.
+
+    &gt;&gt;&gt; async def send(bus):
+    ...     print(&#34;Sending messages.&#34;)
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;})
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+    ...     await bus.send({&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;&#39;,
+    ...                     &#39;command&#39;: &#39;get clients&#39;})
+
+    The run function executes the message bus forever. If we want to stop
+    it, we have to explicitly cancel the task:
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     await setup(bus)
+    ...     bus_task = asyncio.create_task(bus.run())
+    ...     await send(bus)
+    ...     await asyncio.sleep(0)
+    ...     bus_task.cancel()
+    &gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+    Setting up.
+    Creating callback for Logger.
+    Creating callback for Client 1.
+    Creating callback for Client 2.
+    Sending messages.
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Logger&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Client 1&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Client 2&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{}], &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;k1&#39;: &#39;Test&#39;}
+    Logger: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+    Client 1: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;target&#39;: &#39;&#39;, &#39;command&#39;: &#39;get clients&#39;}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Logger&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Client 1&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}]}
+    Logger: {&#39;sender&#39;: &#39;&#39;, &#39;client&#39;: &#39;Client 2&#39;, &#39;plugin&#39;: &#39;Test Plugin&#39;,
+             &#39;sends&#39;: [{}], &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}]}
+    &#34;&#34;&#34;
+
+    def __init__(self) -&gt; None:
+        &#34;&#34;&#34;Initialise a new bus without clients.
+
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        self._queue: asyncio.Queue = asyncio.Queue()
+        self._plugins: Dict[str, str] = {}
+        self._send_reg: MessageTemplateRegistry = MessageTemplateRegistry()
+        self._recv_reg: MessageTemplateRegistry = MessageTemplateRegistry()
+        self._callbacks: Dict[str, MessageCallback] = {}
+
+    def register(self, client: str, plugin: str,
+                 sends: Iterable[MessageTemplate],
+                 receives: Iterable[MessageTemplate],
+                 callback: MessageCallback) -&gt; None:
+        &#34;&#34;&#34;Register a client at the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(message)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+        ...                  [],    # send nothing
+        ...                  [{}],  # receive everything
+        ...                  callback)
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                      # send with key &#39;k1&#39; and string value
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                      # receive for this client
+        ...                  callback)
+        ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+        ...                  [{}],  # send arbitrary
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+        ...                      # receive for this client
+        ...                  callback)
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        if not client:
+            raise BusException(&#34;Client name is not allowed to be empty.&#34;)
+        if client in self._plugins:
+            raise BusException(f&#34;Client &#39;{client}&#39; already registered&#34;
+                               &#34; at message bus.&#34;)
+        event = Message(&#39;&#39;)
+        event[&#39;event&#39;] = &#39;registered&#39;
+        event[&#39;client&#39;] = client
+        self._plugins[client] = plugin
+        event[&#39;plugin&#39;] = plugin
+        for template in sends:
+            self._send_reg.insert(template, client)
+        event[&#39;sends&#39;] = self._send_reg.get_templates(client)
+        for template in receives:
+            self._recv_reg.insert(template, client)
+        event[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+        self._callbacks[client] = callback
+        self._queue.put_nowait(event)
+
+    def unregister(self, client: str) -&gt; None:
+        &#34;&#34;&#34;Unregister a client from the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(message)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                  callback)
+        ...     bus.unregister(&#39;Client 1&#39;)
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        if client not in self._plugins:
+            return
+        event = Message(&#39;&#39;)
+        event[&#39;event&#39;] = &#39;unregistered&#39;
+        event[&#39;client&#39;] = client
+        del self._plugins[client]
+        self._send_reg.delete(client)
+        self._recv_reg.delete(client)
+        if client in self._callbacks:
+            del self._callbacks[client]
+        self._queue.put_nowait(event)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run the message bus forever.
+
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus_task = asyncio.create_task(bus.run())
+        ...     bus_task.cancel()
+        &gt;&gt;&gt; asyncio.run(main())
+        &#34;&#34;&#34;
+        while True:
+            message = await self._queue.get()
+            if (&#39;target&#39; in message and
+                    message[&#39;target&#39;] == &#39;&#39; and
+                    &#39;command&#39; in message and
+                    message[&#39;command&#39;] == &#39;get clients&#39;):
+                for client in self._plugins:
+                    answer = Message(&#39;&#39;)
+                    answer[&#39;client&#39;] = client
+                    answer[&#39;plugin&#39;] = self._plugins[client]
+                    answer[&#39;sends&#39;] = self._send_reg.get_templates(client)
+                    answer[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+                    await self._queue.put(answer)
+            for client in self._recv_reg.get(message):
+                await self._callbacks[client](message)
+            self._queue.task_done()
+
+    async def send(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Send a message to the message bus.
+
+        &gt;&gt;&gt; async def callback(message):
+        ...     print(f&#34;Got: {message}&#34;)
+        &gt;&gt;&gt; async def main():
+        ...     bus = MessageBus()
+        ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+        ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+        ...                  callback)
+        ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+        ...                  [{}],
+        ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+        ...                  callback)
+        ...     bus_task = asyncio.create_task(bus.run())
+        ...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+        ...                     &#39;k1&#39;: &#39;Test&#39;})
+        ...     await bus.send({&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+        ...     try:
+        ...         await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+        ...                         &#39;k1&#39;: 42})
+        ...     except BusException as e:
+        ...         print(e)
+        ...     await asyncio.sleep(0)
+        ...     bus_task.cancel()
+        &gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+        Message &#39;{&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: 42}&#39;
+        not allowed for sender &#39;Client 1&#39;.
+        Got: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: &#39;Test&#39;}
+        Got: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+        &#34;&#34;&#34;
+        assert isinstance(message[&#39;sender&#39;], str)
+        sender = message[&#39;sender&#39;]
+        if sender:
+            if not self._send_reg.check(sender, message):
+                raise BusException(f&#34;Message &#39;{message}&#39; not allowed for&#34;
+                                   f&#34; sender &#39;{sender}&#39;.&#34;)
+        await self._queue.put(message)</code></pre>
+</details>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi.messagebus.MessageBus.register"><code class="name flex">
+<span>def <span class="ident">register</span></span>(<span>self, client: str, plugin: str, sends: Iterable[<a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a>], receives: Iterable[<a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a>], callback: Callable[[ForwardRef('<a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>')], Coroutine[Any, Any, NoneType]]) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register a client at the message bus.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def callback(message):
+...     print(message)
+&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     bus.register('Logger', 'Test Plugin',
+...                  [],    # send nothing
+...                  [{}],  # receive everything
+...                  callback)
+...     bus.register('Client 1', 'Test Plugin',
+...                  [{'k1': {'type': 'string'}}],
+...                      # send with key 'k1' and string value
+...                  [{'target': {'const': 'Client 1'}}],
+...                      # receive for this client
+...                  callback)
+...     bus.register('Client 2', 'Test Plugin',
+...                  [{}],  # send arbitrary
+...                  [{'target': {'const': 'Client 2'}}],
+...                      # receive for this client
+...                  callback)
+&gt;&gt;&gt; asyncio.run(main())
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def register(self, client: str, plugin: str,
+             sends: Iterable[MessageTemplate],
+             receives: Iterable[MessageTemplate],
+             callback: MessageCallback) -&gt; None:
+    &#34;&#34;&#34;Register a client at the message bus.
+
+    &gt;&gt;&gt; async def callback(message):
+    ...     print(message)
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     bus.register(&#39;Logger&#39;, &#39;Test Plugin&#39;,
+    ...                  [],    # send nothing
+    ...                  [{}],  # receive everything
+    ...                  callback)
+    ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+    ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+    ...                      # send with key &#39;k1&#39; and string value
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+    ...                      # receive for this client
+    ...                  callback)
+    ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+    ...                  [{}],  # send arbitrary
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+    ...                      # receive for this client
+    ...                  callback)
+    &gt;&gt;&gt; asyncio.run(main())
+    &#34;&#34;&#34;
+    if not client:
+        raise BusException(&#34;Client name is not allowed to be empty.&#34;)
+    if client in self._plugins:
+        raise BusException(f&#34;Client &#39;{client}&#39; already registered&#34;
+                           &#34; at message bus.&#34;)
+    event = Message(&#39;&#39;)
+    event[&#39;event&#39;] = &#39;registered&#39;
+    event[&#39;client&#39;] = client
+    self._plugins[client] = plugin
+    event[&#39;plugin&#39;] = plugin
+    for template in sends:
+        self._send_reg.insert(template, client)
+    event[&#39;sends&#39;] = self._send_reg.get_templates(client)
+    for template in receives:
+        self._recv_reg.insert(template, client)
+    event[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+    self._callbacks[client] = callback
+    self._queue.put_nowait(event)</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageBus.unregister"><code class="name flex">
+<span>def <span class="ident">unregister</span></span>(<span>self, client: str) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Unregister a client from the message bus.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def callback(message):
+...     print(message)
+&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     bus.register('Client 1', 'Test Plugin',
+...                  [{'k1': {'type': 'string'}}],
+...                  [{'target': {'const': 'Client 1'}}],
+...                  callback)
+...     bus.unregister('Client 1')
+&gt;&gt;&gt; asyncio.run(main())
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def unregister(self, client: str) -&gt; None:
+    &#34;&#34;&#34;Unregister a client from the message bus.
+
+    &gt;&gt;&gt; async def callback(message):
+    ...     print(message)
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+    ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+    ...                  callback)
+    ...     bus.unregister(&#39;Client 1&#39;)
+    &gt;&gt;&gt; asyncio.run(main())
+    &#34;&#34;&#34;
+    if client not in self._plugins:
+        return
+    event = Message(&#39;&#39;)
+    event[&#39;event&#39;] = &#39;unregistered&#39;
+    event[&#39;client&#39;] = client
+    del self._plugins[client]
+    self._send_reg.delete(client)
+    self._recv_reg.delete(client)
+    if client in self._callbacks:
+        del self._callbacks[client]
+    self._queue.put_nowait(event)</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageBus.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run the message bus forever.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     bus_task = asyncio.create_task(bus.run())
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(main())
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run the message bus forever.
+
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     bus_task = asyncio.create_task(bus.run())
+    ...     bus_task.cancel()
+    &gt;&gt;&gt; asyncio.run(main())
+    &#34;&#34;&#34;
+    while True:
+        message = await self._queue.get()
+        if (&#39;target&#39; in message and
+                message[&#39;target&#39;] == &#39;&#39; and
+                &#39;command&#39; in message and
+                message[&#39;command&#39;] == &#39;get clients&#39;):
+            for client in self._plugins:
+                answer = Message(&#39;&#39;)
+                answer[&#39;client&#39;] = client
+                answer[&#39;plugin&#39;] = self._plugins[client]
+                answer[&#39;sends&#39;] = self._send_reg.get_templates(client)
+                answer[&#39;receives&#39;] = self._recv_reg.get_templates(client)
+                await self._queue.put(answer)
+        for client in self._recv_reg.get(message):
+            await self._callbacks[client](message)
+        self._queue.task_done()</code></pre>
+</details>
+</dd>
+<dt id="controlpi.messagebus.MessageBus.send"><code class="name flex">
+<span>async def <span class="ident">send</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Send a message to the message bus.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; async def callback(message):
+...     print(f&quot;Got: {message}&quot;)
+&gt;&gt;&gt; async def main():
+...     bus = MessageBus()
+...     bus.register('Client 1', 'Test Plugin',
+...                  [{'k1': {'type': 'string'}}],
+...                  [{'target': {'const': 'Client 1'}}],
+...                  callback)
+...     bus.register('Client 2', 'Test Plugin',
+...                  [{}],
+...                  [{'target': {'const': 'Client 2'}}],
+...                  callback)
+...     bus_task = asyncio.create_task(bus.run())
+...     await bus.send({'sender': 'Client 1', 'target': 'Client 2',
+...                     'k1': 'Test'})
+...     await bus.send({'sender': 'Client 2', 'target': 'Client 1'})
+...     try:
+...         await bus.send({'sender': 'Client 1', 'target': 'Client 2',
+...                         'k1': 42})
+...     except BusException as e:
+...         print(e)
+...     await asyncio.sleep(0)
+...     bus_task.cancel()
+&gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+Message '{'sender': 'Client 1', 'target': 'Client 2', 'k1': 42}'
+not allowed for sender 'Client 1'.
+Got: {'sender': 'Client 1', 'target': 'Client 2', 'k1': 'Test'}
+Got: {'sender': 'Client 2', 'target': 'Client 1'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def send(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Send a message to the message bus.
+
+    &gt;&gt;&gt; async def callback(message):
+    ...     print(f&#34;Got: {message}&#34;)
+    &gt;&gt;&gt; async def main():
+    ...     bus = MessageBus()
+    ...     bus.register(&#39;Client 1&#39;, &#39;Test Plugin&#39;,
+    ...                  [{&#39;k1&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 1&#39;}}],
+    ...                  callback)
+    ...     bus.register(&#39;Client 2&#39;, &#39;Test Plugin&#39;,
+    ...                  [{}],
+    ...                  [{&#39;target&#39;: {&#39;const&#39;: &#39;Client 2&#39;}}],
+    ...                  callback)
+    ...     bus_task = asyncio.create_task(bus.run())
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+    ...                     &#39;k1&#39;: &#39;Test&#39;})
+    ...     await bus.send({&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;})
+    ...     try:
+    ...         await bus.send({&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;,
+    ...                         &#39;k1&#39;: 42})
+    ...     except BusException as e:
+    ...         print(e)
+    ...     await asyncio.sleep(0)
+    ...     bus_task.cancel()
+    &gt;&gt;&gt; asyncio.run(main())  # doctest: +NORMALIZE_WHITESPACE
+    Message &#39;{&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: 42}&#39;
+    not allowed for sender &#39;Client 1&#39;.
+    Got: {&#39;sender&#39;: &#39;Client 1&#39;, &#39;target&#39;: &#39;Client 2&#39;, &#39;k1&#39;: &#39;Test&#39;}
+    Got: {&#39;sender&#39;: &#39;Client 2&#39;, &#39;target&#39;: &#39;Client 1&#39;}
+    &#34;&#34;&#34;
+    assert isinstance(message[&#39;sender&#39;], str)
+    sender = message[&#39;sender&#39;]
+    if sender:
+        if not self._send_reg.check(sender, message):
+            raise BusException(f&#34;Message &#39;{message}&#39; not allowed for&#34;
+                               f&#34; sender &#39;{sender}&#39;.&#34;)
+    await self._queue.put(message)</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi" href="index.html">controlpi</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi.messagebus.Message" href="#controlpi.messagebus.Message">Message</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi.messagebus.Message.check_value" href="#controlpi.messagebus.Message.check_value">check_value</a></code></li>
+<li><code><a title="controlpi.messagebus.Message.update" href="#controlpi.messagebus.Message.update">update</a></code></li>
+<li><code><a title="controlpi.messagebus.Message.setdefault" href="#controlpi.messagebus.Message.setdefault">setdefault</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi.messagebus.MessageTemplate" href="#controlpi.messagebus.MessageTemplate">MessageTemplate</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi.messagebus.MessageTemplate.from_message" href="#controlpi.messagebus.MessageTemplate.from_message">from_message</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplate.update" href="#controlpi.messagebus.MessageTemplate.update">update</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplate.setdefault" href="#controlpi.messagebus.MessageTemplate.setdefault">setdefault</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplate.check" href="#controlpi.messagebus.MessageTemplate.check">check</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi.messagebus.MessageTemplateRegistry" href="#controlpi.messagebus.MessageTemplateRegistry">MessageTemplateRegistry</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi.messagebus.MessageTemplateRegistry.insert" href="#controlpi.messagebus.MessageTemplateRegistry.insert">insert</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplateRegistry.delete" href="#controlpi.messagebus.MessageTemplateRegistry.delete">delete</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplateRegistry.check" href="#controlpi.messagebus.MessageTemplateRegistry.check">check</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplateRegistry.get" href="#controlpi.messagebus.MessageTemplateRegistry.get">get</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageTemplateRegistry.get_templates" href="#controlpi.messagebus.MessageTemplateRegistry.get_templates">get_templates</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi.messagebus.BusException" href="#controlpi.messagebus.BusException">BusException</a></code></h4>
+</li>
+<li>
+<h4><code><a title="controlpi.messagebus.MessageBus" href="#controlpi.messagebus.MessageBus">MessageBus</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi.messagebus.MessageBus.register" href="#controlpi.messagebus.MessageBus.register">register</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageBus.unregister" href="#controlpi.messagebus.MessageBus.unregister">unregister</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageBus.run" href="#controlpi.messagebus.MessageBus.run">run</a></code></li>
+<li><code><a title="controlpi.messagebus.MessageBus.send" href="#controlpi.messagebus.MessageBus.send">send</a></code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi/pluginregistry.html b/doc/controlpi/pluginregistry.html
new file mode 100644 (file)
index 0000000..ad9a464
--- /dev/null
@@ -0,0 +1,429 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi.pluginregistry API documentation</title>
+<meta name="description" content="Provide a generic plugin system …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi.pluginregistry</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide a generic plugin system.</p>
+<p>The class PluginRegistry is initialised with the name of a namespace
+package and a base class.</p>
+<p>All modules in the namespace package are loaded. These modules can be
+included in different distribution packages, which allows to dynamically
+add plugins to the system without changing any code.</p>
+<p>Afterwards, all (direct and indirect) subclasses of the base class are
+registered as plugins under their class name. Class names should be unique,
+which cannot be programmatically enforced.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class BasePlugin:
+...     pass
+&gt;&gt;&gt; class Plugin1(BasePlugin):
+...     pass
+&gt;&gt;&gt; class Plugin2(BasePlugin):
+...     pass
+&gt;&gt;&gt; registry = PluginRegistry('importlib', BasePlugin)
+</code></pre>
+<p>The registry provides a generic mapping interface with the class names as
+keys and the classes as values.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; print(len(registry))
+2
+&gt;&gt;&gt; for name in registry:
+...     print(f&quot;{name}: {registry[name]}&quot;)
+Plugin1: &lt;class 'pluginregistry.Plugin1'&gt;
+Plugin2: &lt;class 'pluginregistry.Plugin2'&gt;
+&gt;&gt;&gt; if 'Plugin1' in registry:
+...     print(f&quot;'Plugin1' is in registry.&quot;)
+'Plugin1' is in registry.
+&gt;&gt;&gt; p1 = registry['Plugin1']
+&gt;&gt;&gt; i1 = p1()
+</code></pre>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide a generic plugin system.
+
+The class PluginRegistry is initialised with the name of a namespace
+package and a base class.
+
+All modules in the namespace package are loaded. These modules can be
+included in different distribution packages, which allows to dynamically
+add plugins to the system without changing any code.
+
+Afterwards, all (direct and indirect) subclasses of the base class are
+registered as plugins under their class name. Class names should be unique,
+which cannot be programmatically enforced.
+
+&gt;&gt;&gt; class BasePlugin:
+...     pass
+&gt;&gt;&gt; class Plugin1(BasePlugin):
+...     pass
+&gt;&gt;&gt; class Plugin2(BasePlugin):
+...     pass
+&gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+
+The registry provides a generic mapping interface with the class names as
+keys and the classes as values.
+
+&gt;&gt;&gt; print(len(registry))
+2
+&gt;&gt;&gt; for name in registry:
+...     print(f&#34;{name}: {registry[name]}&#34;)
+Plugin1: &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+Plugin2: &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+&gt;&gt;&gt; if &#39;Plugin1&#39; in registry:
+...     print(f&#34;&#39;Plugin1&#39; is in registry.&#34;)
+&#39;Plugin1&#39; is in registry.
+&gt;&gt;&gt; p1 = registry[&#39;Plugin1&#39;]
+&gt;&gt;&gt; i1 = p1()
+&#34;&#34;&#34;
+import importlib
+import pkgutil
+import collections.abc
+
+
+class PluginRegistry(collections.abc.Mapping):
+    &#34;&#34;&#34;Provide a registry for plugins.
+
+    Initialise the registry by loading all modules in the given namespace
+    package and then registering all subclasses of the given base class as
+    plugins (only simulated here – the code for Plugin1 and Plugin2 should
+    be in modules in the given namespace package in real applications):
+    &gt;&gt;&gt; class BasePlugin:
+    ...     pass
+    &gt;&gt;&gt; class Plugin1(BasePlugin):
+    ...     pass
+    &gt;&gt;&gt; class Plugin2(BasePlugin):
+    ...     pass
+    &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+
+    After initialisation, provide a mapping interface to the plugins:
+    &gt;&gt;&gt; print(len(registry))
+    2
+    &gt;&gt;&gt; for name in registry:
+    ...     print(f&#34;{name}: {registry[name]}&#34;)
+    Plugin1: &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+    Plugin2: &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+    &gt;&gt;&gt; if &#39;Plugin1&#39; in registry:
+    ...     print(f&#34;&#39;Plugin1&#39; is in registry.&#34;)
+    &#39;Plugin1&#39; is in registry.
+    &#34;&#34;&#34;
+
+    def __init__(self, namespace_package: str, base_class: type) -&gt; None:
+        &#34;&#34;&#34;Initialise registry.
+
+        Import all modules defined in the given namespace package (in any
+        distribution package currently installed in the path). Then register
+        all subclasses of the given base class as plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; for name in registry._plugins:
+        ...     print(f&#34;{name}: {registry._plugins[name]}&#34;)
+        Plugin1: &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        Plugin2: &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &#34;&#34;&#34;
+        ns_mod = importlib.import_module(namespace_package)
+        ns_path = ns_mod.__path__  # type: ignore  # mypy issue #1422
+        ns_name = ns_mod.__name__
+        for _, mod_name, _ in pkgutil.iter_modules(ns_path):
+            importlib.import_module(f&#34;{ns_name}.{mod_name}&#34;)
+
+        def all_subclasses(cls):
+            result = []
+            for subcls in cls.__subclasses__():
+                result.append(subcls)
+                result.extend(all_subclasses(subcls))
+            return result
+        self._plugins = {cls.__name__: cls
+                         for cls in all_subclasses(base_class)}
+
+    def __len__(self) -&gt; int:
+        &#34;&#34;&#34;Get number of registered plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(registry.__len__())
+        2
+        &#34;&#34;&#34;
+        return len(self._plugins)
+
+    def __iter__(self):
+        &#34;&#34;&#34;Get an iterator of the registered plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(type(registry.__iter__()))
+        &lt;class &#39;dict_keyiterator&#39;&gt;
+        &gt;&gt;&gt; for name in registry:
+        ...     print(name)
+        Plugin1
+        Plugin2
+        &#34;&#34;&#34;
+        return iter(self._plugins)
+
+    def __getitem__(self, plugin_name: str) -&gt; type:
+        &#34;&#34;&#34;Get a registered plugin given its name.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(registry.__getitem__(&#39;Plugin1&#39;))
+        &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        &gt;&gt;&gt; print(registry.__getitem__(&#39;Plugin2&#39;))
+        &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &gt;&gt;&gt; for name in registry:
+        ...     print(registry[name])
+        &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &#34;&#34;&#34;
+        return self._plugins[plugin_name]</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi.pluginregistry.PluginRegistry"><code class="flex name class">
+<span>class <span class="ident">PluginRegistry</span></span>
+<span>(</span><span>namespace_package: str, base_class: type)</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Provide a registry for plugins.</p>
+<p>Initialise the registry by loading all modules in the given namespace
+package and then registering all subclasses of the given base class as
+plugins (only simulated here – the code for Plugin1 and Plugin2 should
+be in modules in the given namespace package in real applications):</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class BasePlugin:
+...     pass
+&gt;&gt;&gt; class Plugin1(BasePlugin):
+...     pass
+&gt;&gt;&gt; class Plugin2(BasePlugin):
+...     pass
+&gt;&gt;&gt; registry = PluginRegistry('importlib', BasePlugin)
+</code></pre>
+<p>After initialisation, provide a mapping interface to the plugins:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; print(len(registry))
+2
+&gt;&gt;&gt; for name in registry:
+...     print(f&quot;{name}: {registry[name]}&quot;)
+Plugin1: &lt;class 'pluginregistry.Plugin1'&gt;
+Plugin2: &lt;class 'pluginregistry.Plugin2'&gt;
+&gt;&gt;&gt; if 'Plugin1' in registry:
+...     print(f&quot;'Plugin1' is in registry.&quot;)
+'Plugin1' is in registry.
+</code></pre>
+<p>Initialise registry.</p>
+<p>Import all modules defined in the given namespace package (in any
+distribution package currently installed in the path). Then register
+all subclasses of the given base class as plugins.</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; class BasePlugin:
+...     pass
+&gt;&gt;&gt; class Plugin1(BasePlugin):
+...     pass
+&gt;&gt;&gt; class Plugin2(BasePlugin):
+...     pass
+&gt;&gt;&gt; registry = PluginRegistry('importlib', BasePlugin)
+&gt;&gt;&gt; for name in registry._plugins:
+...     print(f&quot;{name}: {registry._plugins[name]}&quot;)
+Plugin1: &lt;class 'pluginregistry.Plugin1'&gt;
+Plugin2: &lt;class 'pluginregistry.Plugin2'&gt;
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class PluginRegistry(collections.abc.Mapping):
+    &#34;&#34;&#34;Provide a registry for plugins.
+
+    Initialise the registry by loading all modules in the given namespace
+    package and then registering all subclasses of the given base class as
+    plugins (only simulated here – the code for Plugin1 and Plugin2 should
+    be in modules in the given namespace package in real applications):
+    &gt;&gt;&gt; class BasePlugin:
+    ...     pass
+    &gt;&gt;&gt; class Plugin1(BasePlugin):
+    ...     pass
+    &gt;&gt;&gt; class Plugin2(BasePlugin):
+    ...     pass
+    &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+
+    After initialisation, provide a mapping interface to the plugins:
+    &gt;&gt;&gt; print(len(registry))
+    2
+    &gt;&gt;&gt; for name in registry:
+    ...     print(f&#34;{name}: {registry[name]}&#34;)
+    Plugin1: &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+    Plugin2: &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+    &gt;&gt;&gt; if &#39;Plugin1&#39; in registry:
+    ...     print(f&#34;&#39;Plugin1&#39; is in registry.&#34;)
+    &#39;Plugin1&#39; is in registry.
+    &#34;&#34;&#34;
+
+    def __init__(self, namespace_package: str, base_class: type) -&gt; None:
+        &#34;&#34;&#34;Initialise registry.
+
+        Import all modules defined in the given namespace package (in any
+        distribution package currently installed in the path). Then register
+        all subclasses of the given base class as plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; for name in registry._plugins:
+        ...     print(f&#34;{name}: {registry._plugins[name]}&#34;)
+        Plugin1: &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        Plugin2: &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &#34;&#34;&#34;
+        ns_mod = importlib.import_module(namespace_package)
+        ns_path = ns_mod.__path__  # type: ignore  # mypy issue #1422
+        ns_name = ns_mod.__name__
+        for _, mod_name, _ in pkgutil.iter_modules(ns_path):
+            importlib.import_module(f&#34;{ns_name}.{mod_name}&#34;)
+
+        def all_subclasses(cls):
+            result = []
+            for subcls in cls.__subclasses__():
+                result.append(subcls)
+                result.extend(all_subclasses(subcls))
+            return result
+        self._plugins = {cls.__name__: cls
+                         for cls in all_subclasses(base_class)}
+
+    def __len__(self) -&gt; int:
+        &#34;&#34;&#34;Get number of registered plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(registry.__len__())
+        2
+        &#34;&#34;&#34;
+        return len(self._plugins)
+
+    def __iter__(self):
+        &#34;&#34;&#34;Get an iterator of the registered plugins.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(type(registry.__iter__()))
+        &lt;class &#39;dict_keyiterator&#39;&gt;
+        &gt;&gt;&gt; for name in registry:
+        ...     print(name)
+        Plugin1
+        Plugin2
+        &#34;&#34;&#34;
+        return iter(self._plugins)
+
+    def __getitem__(self, plugin_name: str) -&gt; type:
+        &#34;&#34;&#34;Get a registered plugin given its name.
+
+        &gt;&gt;&gt; class BasePlugin:
+        ...     pass
+        &gt;&gt;&gt; class Plugin1(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; class Plugin2(BasePlugin):
+        ...     pass
+        &gt;&gt;&gt; registry = PluginRegistry(&#39;importlib&#39;, BasePlugin)
+        &gt;&gt;&gt; print(registry.__getitem__(&#39;Plugin1&#39;))
+        &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        &gt;&gt;&gt; print(registry.__getitem__(&#39;Plugin2&#39;))
+        &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &gt;&gt;&gt; for name in registry:
+        ...     print(registry[name])
+        &lt;class &#39;pluginregistry.Plugin1&#39;&gt;
+        &lt;class &#39;pluginregistry.Plugin2&#39;&gt;
+        &#34;&#34;&#34;
+        return self._plugins[plugin_name]</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>collections.abc.Mapping</li>
+<li>collections.abc.Collection</li>
+<li>collections.abc.Sized</li>
+<li>collections.abc.Iterable</li>
+<li>collections.abc.Container</li>
+</ul>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi" href="index.html">controlpi</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi.pluginregistry.PluginRegistry" href="#controlpi.pluginregistry.PluginRegistry">PluginRegistry</a></code></h4>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi_plugins/index.html b/doc/controlpi_plugins/index.html
new file mode 100644 (file)
index 0000000..6775c5d
--- /dev/null
@@ -0,0 +1,70 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi_plugins API documentation</title>
+<meta name="description" content="" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Namespace <code>controlpi_plugins</code></h1>
+</header>
+<section id="section-intro">
+</section>
+<section>
+<h2 class="section-title" id="header-submodules">Sub-modules</h2>
+<dl>
+<dt><code class="name"><a title="controlpi_plugins.state" href="state.html">controlpi_plugins.state</a></code></dt>
+<dd>
+<div class="desc"><p>Provide state plugins for all kinds of systems …</p></div>
+</dd>
+<dt><code class="name"><a title="controlpi_plugins.util" href="util.html">controlpi_plugins.util</a></code></dt>
+<dd>
+<div class="desc"><p>Provide utility plugins for all kinds of systems …</p></div>
+</dd>
+<dt><code class="name"><a title="controlpi_plugins.wait" href="wait.html">controlpi_plugins.wait</a></code></dt>
+<dd>
+<div class="desc"><p>Provide waiting/sleeping plugins for all kinds of systems …</p></div>
+</dd>
+</dl>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3><a href="#header-submodules">Sub-modules</a></h3>
+<ul>
+<li><code><a title="controlpi_plugins.state" href="state.html">controlpi_plugins.state</a></code></li>
+<li><code><a title="controlpi_plugins.util" href="util.html">controlpi_plugins.util</a></code></li>
+<li><code><a title="controlpi_plugins.wait" href="wait.html">controlpi_plugins.wait</a></code></li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi_plugins/state.html b/doc/controlpi_plugins/state.html
new file mode 100644 (file)
index 0000000..8f33480
--- /dev/null
@@ -0,0 +1,1884 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi_plugins.state API documentation</title>
+<meta name="description" content="Provide state plugins for all kinds of systems …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi_plugins.state</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide state plugins for all kinds of systems.</p>
+<ul>
+<li>State represents a Boolean state.</li>
+<li>StateAlias translates to another state-like client.</li>
+<li>AndState combines several state-like clients by conjunction.</li>
+<li>OrState combines several state-like clients by disjunction.</li>
+</ul>
+<p>All these plugins use the following conventions:</p>
+<ul>
+<li>If their state changes they send a message containing "event": "changed"
+and "state": <new state>.</li>
+<li>If their state is reported due to a message, but did not change they send
+a message containing just "state": <current state>.</li>
+<li>If they receive a message containing "target": <name> and
+"command": "get state" they report their current state.</li>
+<li>If State (or any other settable state using these conventions) receives
+a message containing "target": <name>, "command": "set state" and
+"new state": <state to be set> it changes the state accordingly. If this
+was really a change the corresponding event is sent. If it was already in
+this state a report message without "event": "changed" is sent.</li>
+<li>StateAlias can alias any message bus client using these conventions, not
+just State instances. It translates all messages described here in both
+directions.</li>
+<li>AndState and OrState instances cannot be set.</li>
+<li>AndState and OrState can combine any message bus clients using these
+conventions, not just State instances. They only react to messages
+containing "state" information.</li>
+</ul>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test State&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test State 2&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test StateAlias&quot;: {&quot;plugin&quot;: &quot;StateAlias&quot;,
+...                          &quot;alias for&quot;: &quot;Test State 2&quot;},
+...      &quot;Test AndState&quot;: {&quot;plugin&quot;: &quot;AndState&quot;,
+...                        &quot;states&quot;: [&quot;Test State&quot;, &quot;Test StateAlias&quot;]},
+...      &quot;Test OrState&quot;: {&quot;plugin&quot;: &quot;OrState&quot;,
+...                       &quot;states&quot;: [&quot;Test State&quot;, &quot;Test StateAlias&quot;]}},
+...     [{&quot;target&quot;: &quot;Test AndState&quot;,
+...       &quot;command&quot;: &quot;get state&quot;},
+...      {&quot;target&quot;: &quot;Test OrState&quot;,
+...       &quot;command&quot;: &quot;get state&quot;},
+...      {&quot;target&quot;: &quot;Test State&quot;,
+...       &quot;command&quot;: &quot;set state&quot;, &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test StateAlias&quot;,
+...       &quot;command&quot;: &quot;set state&quot;, &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State&quot;,
+...       &quot;command&quot;: &quot;set state&quot;, &quot;new state&quot;: False}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State 2', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State 2'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State 2'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test StateAlias', 'plugin': 'StateAlias',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}},
+                   {'target': {'const': 'Test State 2'},
+                    'command': {'const': 'get state'}},
+                   {'target': {'const': 'Test State 2'},
+                    'command': {'const': 'set state'},
+                    'new state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test StateAlias'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test StateAlias'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State 2'},
+                       'event': {'const': 'changed'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State 2'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test AndState', 'plugin': 'AndState',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test AndState'},
+                       'command': {'const': 'get state'}},
+                      {'sender': {'const': 'Test State'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test StateAlias'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test OrState', 'plugin': 'OrState',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test OrState'},
+                       'command': {'const': 'get state'}},
+                      {'sender': {'const': 'Test State'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test StateAlias'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': 'test()', 'target': 'Test AndState',
+         'command': 'get state'}
+test(): {'sender': 'Test AndState', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test OrState',
+         'command': 'get state'}
+test(): {'sender': 'Test OrState', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test StateAlias',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test StateAlias', 'target': 'Test State 2',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'set state', 'new state': False}
+test(): {'sender': 'Test State', 'event': 'changed', 'state': False}
+test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False}
+</code></pre>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide state plugins for all kinds of systems.
+
+- State represents a Boolean state.
+- StateAlias translates to another state-like client.
+- AndState combines several state-like clients by conjunction.
+- OrState combines several state-like clients by disjunction.
+
+All these plugins use the following conventions:
+
+- If their state changes they send a message containing &#34;event&#34;: &#34;changed&#34;
+  and &#34;state&#34;: &lt;new state&gt;.
+- If their state is reported due to a message, but did not change they send
+  a message containing just &#34;state&#34;: &lt;current state&gt;.
+- If they receive a message containing &#34;target&#34;: &lt;name&gt; and
+  &#34;command&#34;: &#34;get state&#34; they report their current state.
+- If State (or any other settable state using these conventions) receives
+  a message containing &#34;target&#34;: &lt;name&gt;, &#34;command&#34;: &#34;set state&#34; and
+  &#34;new state&#34;: &lt;state to be set&gt; it changes the state accordingly. If this
+  was really a change the corresponding event is sent. If it was already in
+  this state a report message without &#34;event&#34;: &#34;changed&#34; is sent.
+- StateAlias can alias any message bus client using these conventions, not
+  just State instances. It translates all messages described here in both
+  directions.
+- AndState and OrState instances cannot be set.
+- AndState and OrState can combine any message bus clients using these
+  conventions, not just State instances. They only react to messages
+  containing &#34;state&#34; information.
+
+&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&#34;Test State&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+...      &#34;Test State 2&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+...      &#34;Test StateAlias&#34;: {&#34;plugin&#34;: &#34;StateAlias&#34;,
+...                          &#34;alias for&#34;: &#34;Test State 2&#34;},
+...      &#34;Test AndState&#34;: {&#34;plugin&#34;: &#34;AndState&#34;,
+...                        &#34;states&#34;: [&#34;Test State&#34;, &#34;Test StateAlias&#34;]},
+...      &#34;Test OrState&#34;: {&#34;plugin&#34;: &#34;OrState&#34;,
+...                       &#34;states&#34;: [&#34;Test State&#34;, &#34;Test StateAlias&#34;]}},
+...     [{&#34;target&#34;: &#34;Test AndState&#34;,
+...       &#34;command&#34;: &#34;get state&#34;},
+...      {&#34;target&#34;: &#34;Test OrState&#34;,
+...       &#34;command&#34;: &#34;get state&#34;},
+...      {&#34;target&#34;: &#34;Test State&#34;,
+...       &#34;command&#34;: &#34;set state&#34;, &#34;new state&#34;: True},
+...      {&#34;target&#34;: &#34;Test StateAlias&#34;,
+...       &#34;command&#34;: &#34;set state&#34;, &#34;new state&#34;: True},
+...      {&#34;target&#34;: &#34;Test State&#34;,
+...       &#34;command&#34;: &#34;set state&#34;, &#34;new state&#34;: False}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test State&#39;, &#39;plugin&#39;: &#39;State&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                    &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                      {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                       &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test State 2&#39;, &#39;plugin&#39;: &#39;State&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                    &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                      {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                       &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test StateAlias&#39;, &#39;plugin&#39;: &#39;StateAlias&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                    &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                    &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                   {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                    &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                    &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                      {&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                       &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                       &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test AndState&#39;, &#39;plugin&#39;: &#39;AndState&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                    &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test AndState&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test OrState&#39;, &#39;plugin&#39;: &#39;OrState&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                    &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                   {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test OrState&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                      {&#39;sender&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                       &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test AndState&#39;,
+         &#39;command&#39;: &#39;get state&#39;}
+test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;state&#39;: False}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test OrState&#39;,
+         &#39;command&#39;: &#39;get state&#39;}
+test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;state&#39;: False}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+         &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test StateAlias&#39;,
+         &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;target&#39;: &#39;Test State 2&#39;,
+         &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test State 2&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+         &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: False}
+test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+&#34;&#34;&#34;
+from controlpi import BasePlugin, Message, MessageTemplate
+
+from typing import Dict
+
+
+class State(BasePlugin):
+    &#34;&#34;&#34;Provide a Boolean state.
+
+    The state of a State plugin instance can be queried with the &#34;get state&#34;
+    command and set with the &#34;set state&#34; command to the new state given by
+    the &#34;new state&#34; key:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State&#34;: {&#34;plugin&#34;: &#34;State&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for State plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process commands to get/set state.&#34;&#34;&#34;
+        if message[&#39;command&#39;] == &#39;get state&#39;:
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        elif message[&#39;command&#39;] == &#39;set state&#39;:
+            if self.state != message[&#39;new state&#39;]:
+                assert isinstance(message[&#39;new state&#39;], bool)
+                self.state: bool = message[&#39;new state&#39;]
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+            else:
+                await self.bus.send(Message(self.name,
+                                            {&#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.state = False
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                     &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        self.bus.register(self.name, &#39;State&#39;, sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class StateAlias(BasePlugin):
+    &#34;&#34;&#34;Define an alias for another state.
+
+    The &#34;alias for&#34; configuration key gets the name for the other state that
+    is aliased by the StateAlias plugin instance.
+
+    The &#34;get state&#34; and &#34;set state&#34; commands are forwarded to and the
+    &#34;changed&#34; events and &#34;state&#34; messages are forwarded from this other
+    state:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test StateAlias&#34;: {&#34;plugin&#34;: &#34;StateAlias&#34;,
+    ...                          &#34;alias for&#34;: &#34;Test State&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;},
+    ...      {&#34;target&#34;: &#34;Test StateAlias&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test StateAlias&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test StateAlias&#39;, &#39;plugin&#39;: &#39;StateAlias&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                        &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                       {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                        &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                        &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test StateAlias&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test StateAlias&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;alias for&#39;: {&#39;type&#39;: &#39;string&#39;}},
+                   &#39;required&#39;: [&#39;alias for&#39;]}
+    &#34;&#34;&#34;Schema for StateAlias plugin configuration.
+
+    Required configuration key:
+
+    - &#39;alias for&#39;: name of aliased state.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Translate states from and commands to aliased state.&#34;&#34;&#34;
+        alias_message = Message(self.name)
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message):
+            alias_message[&#39;target&#39;] = self.conf[&#39;alias for&#39;]
+            if message[&#39;command&#39;] == &#39;get state&#39;:
+                alias_message[&#39;command&#39;] = &#39;get state&#39;
+                await self.bus.send(alias_message)
+            elif (message[&#39;command&#39;] == &#39;set state&#39; and
+                    &#39;new state&#39; in message):
+                alias_message[&#39;command&#39;] = &#39;set state&#39;
+                alias_message[&#39;new state&#39;] = message[&#39;new state&#39;]
+                await self.bus.send(alias_message)
+        if (message[&#39;sender&#39;] == self.conf[&#39;alias for&#39;] and
+                &#39;state&#39; in message):
+            if &#39;event&#39; in message and message[&#39;event&#39;] == &#39;changed&#39;:
+                alias_message[&#39;event&#39;] = &#39;changed&#39;
+            alias_message[&#39;state&#39;] = message[&#39;state&#39;]
+            await self.bus.send(alias_message)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                  &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                 MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                  &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                  &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                     &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                    MessageTemplate({&#39;sender&#39;:
+                                     {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                     &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                     &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                    MessageTemplate({&#39;sender&#39;:
+                                     {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                     &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        self.bus.register(self.name, &#39;StateAlias&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class AndState(BasePlugin):
+    &#34;&#34;&#34;Implement conjunction of states.
+
+    The &#34;states&#34; configuration key gets an array of states to be combined.
+    An AndState plugin client reacts to &#34;get state&#34; commands and sends
+    &#34;changed&#34; events when a change in one of the combined states leads to
+    a change for the conjunction:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State 1&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test State 2&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test AndState&#34;: {&#34;plugin&#34;: &#34;AndState&#34;,
+    ...                        &#34;states&#34;: [&#34;Test State 1&#34;, &#34;Test State 2&#34;]}},
+    ...     [{&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 2&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: False},
+    ...      {&#34;target&#34;: &#34;Test AndState&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 1&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 2&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test AndState&#39;, &#39;plugin&#39;: &#39;AndState&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test AndState&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 2&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 2&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test AndState&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;state&#39;: False}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;states&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;string&#39;}}},
+                   &#39;required&#39;: [&#39;states&#39;]}
+    &#34;&#34;&#34;Schema for AndState plugin configuration.
+
+    Required configuration key:
+
+    - &#39;states&#39;: list of names of combined states.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+            assert isinstance(message[&#39;sender&#39;], str)
+            assert isinstance(message[&#39;state&#39;], bool)
+            self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+            new_state = all(self.states.values())
+            if self.state != new_state:
+                self.state: bool = new_state
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+        self.states: Dict[str, bool] = {}
+        for state in self.conf[&#39;states&#39;]:
+            receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                             &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+            self.states[state] = False
+        self.state = all(self.states.values())
+        self.bus.register(self.name, &#39;AndState&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class OrState(BasePlugin):
+    &#34;&#34;&#34;Implement disjunction of states.
+
+    The &#34;states&#34; configuration key gets an array of states to be combined.
+    An OrState plugin client reacts to &#34;get state&#34; commands and sends
+    &#34;changed&#34; events when a change in one of the combined states leads to
+    a change for the disjunction:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State 1&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test State 2&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test OrState&#34;: {&#34;plugin&#34;: &#34;OrState&#34;,
+    ...                       &#34;states&#34;: [&#34;Test State 1&#34;, &#34;Test State 2&#34;]}},
+    ...     [{&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 2&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: False},
+    ...      {&#34;target&#34;: &#34;Test OrState&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 1&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 2&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test OrState&#39;, &#39;plugin&#39;: &#39;OrState&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test OrState&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 2&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 2&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test OrState&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;states&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;string&#39;}}},
+                   &#39;required&#39;: [&#39;states&#39;]}
+    &#34;&#34;&#34;Schema for OrState plugin configuration.
+
+    Required configuration key:
+
+    - &#39;states&#39;: list of names of combined states.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+            assert isinstance(message[&#39;sender&#39;], str)
+            assert isinstance(message[&#39;state&#39;], bool)
+            self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+            new_state = any(self.states.values())
+            if self.state != new_state:
+                self.state: bool = new_state
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+        self.states: Dict[str, bool] = {}
+        for state in self.conf[&#39;states&#39;]:
+            receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                             &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+            self.states[state] = False
+        self.state = any(self.states.values())
+        self.bus.register(self.name, &#39;OrState&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi_plugins.state.State"><code class="flex name class">
+<span>class <span class="ident">State</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Provide a Boolean state.</p>
+<p>The state of a State plugin instance can be queried with the "get state"
+command and set with the "set state" command to the new state given by
+the "new state" key:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test State&quot;: {&quot;plugin&quot;: &quot;State&quot;}},
+...     [{&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;get state&quot;},
+...      {&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;get state&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'get state'}
+test(): {'sender': 'Test State', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'get state'}
+test(): {'sender': 'Test State', 'state': True}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class State(BasePlugin):
+    &#34;&#34;&#34;Provide a Boolean state.
+
+    The state of a State plugin instance can be queried with the &#34;get state&#34;
+    command and set with the &#34;set state&#34; command to the new state given by
+    the &#34;new state&#34; key:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State&#34;: {&#34;plugin&#34;: &#34;State&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for State plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process commands to get/set state.&#34;&#34;&#34;
+        if message[&#39;command&#39;] == &#39;get state&#39;:
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        elif message[&#39;command&#39;] == &#39;set state&#39;:
+            if self.state != message[&#39;new state&#39;]:
+                assert isinstance(message[&#39;new state&#39;], bool)
+                self.state: bool = message[&#39;new state&#39;]
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+            else:
+                await self.bus.send(Message(self.name,
+                                            {&#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.state = False
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                     &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        self.bus.register(self.name, &#39;State&#39;, sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.state.State.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for State plugin configuration.</p>
+<p>There are no required or optional configuration keys.</p></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.state.State.receive"><code class="name flex">
+<span>async def <span class="ident">receive</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Process commands to get/set state.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def receive(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Process commands to get/set state.&#34;&#34;&#34;
+    if message[&#39;command&#39;] == &#39;get state&#39;:
+        await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+    elif message[&#39;command&#39;] == &#39;set state&#39;:
+        if self.state != message[&#39;new state&#39;]:
+            assert isinstance(message[&#39;new state&#39;], bool)
+            self.state: bool = message[&#39;new state&#39;]
+            await self.bus.send(Message(self.name,
+                                        {&#39;event&#39;: &#39;changed&#39;,
+                                         &#39;state&#39;: self.state}))
+        else:
+            await self.bus.send(Message(self.name,
+                                        {&#39;state&#39;: self.state}))</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.State.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    self.state = False
+    sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                              &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+             MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                 &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    self.bus.register(self.name, &#39;State&#39;, sends, receives, self.receive)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.State.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.state.StateAlias"><code class="flex name class">
+<span>class <span class="ident">StateAlias</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Define an alias for another state.</p>
+<p>The "alias for" configuration key gets the name for the other state that
+is aliased by the StateAlias plugin instance.</p>
+<p>The "get state" and "set state" commands are forwarded to and the
+"changed" events and "state" messages are forwarded from this other
+state:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test State&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test StateAlias&quot;: {&quot;plugin&quot;: &quot;StateAlias&quot;,
+...                          &quot;alias for&quot;: &quot;Test State&quot;}},
+...     [{&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;get state&quot;},
+...      {&quot;target&quot;: &quot;Test StateAlias&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test StateAlias&quot;, &quot;command&quot;: &quot;get state&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test StateAlias', 'plugin': 'StateAlias',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}},
+                   {'target': {'const': 'Test State'},
+                    'command': {'const': 'get state'}},
+                   {'target': {'const': 'Test State'},
+                    'command': {'const': 'set state'},
+                    'new state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test StateAlias'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test StateAlias'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State'},
+                       'event': {'const': 'changed'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'get state'}
+test(): {'sender': 'Test State', 'state': False}
+test(): {'sender': 'Test StateAlias', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test StateAlias',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test StateAlias', 'target': 'Test State',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test StateAlias', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State', 'state': True}
+test(): {'sender': 'Test StateAlias', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test StateAlias',
+         'command': 'get state'}
+test(): {'sender': 'Test StateAlias', 'target': 'Test State',
+         'command': 'get state'}
+test(): {'sender': 'Test State', 'state': True}
+test(): {'sender': 'Test StateAlias', 'state': True}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class StateAlias(BasePlugin):
+    &#34;&#34;&#34;Define an alias for another state.
+
+    The &#34;alias for&#34; configuration key gets the name for the other state that
+    is aliased by the StateAlias plugin instance.
+
+    The &#34;get state&#34; and &#34;set state&#34; commands are forwarded to and the
+    &#34;changed&#34; events and &#34;state&#34; messages are forwarded from this other
+    state:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test StateAlias&#34;: {&#34;plugin&#34;: &#34;StateAlias&#34;,
+    ...                          &#34;alias for&#34;: &#34;Test State&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;get state&#34;},
+    ...      {&#34;target&#34;: &#34;Test StateAlias&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test StateAlias&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test StateAlias&#39;, &#39;plugin&#39;: &#39;StateAlias&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                        &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                       {&#39;target&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                        &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                        &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test StateAlias&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test StateAlias&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test StateAlias&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;target&#39;: &#39;Test State&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test State&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test StateAlias&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;alias for&#39;: {&#39;type&#39;: &#39;string&#39;}},
+                   &#39;required&#39;: [&#39;alias for&#39;]}
+    &#34;&#34;&#34;Schema for StateAlias plugin configuration.
+
+    Required configuration key:
+
+    - &#39;alias for&#39;: name of aliased state.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Translate states from and commands to aliased state.&#34;&#34;&#34;
+        alias_message = Message(self.name)
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message):
+            alias_message[&#39;target&#39;] = self.conf[&#39;alias for&#39;]
+            if message[&#39;command&#39;] == &#39;get state&#39;:
+                alias_message[&#39;command&#39;] = &#39;get state&#39;
+                await self.bus.send(alias_message)
+            elif (message[&#39;command&#39;] == &#39;set state&#39; and
+                    &#39;new state&#39; in message):
+                alias_message[&#39;command&#39;] = &#39;set state&#39;
+                alias_message[&#39;new state&#39;] = message[&#39;new state&#39;]
+                await self.bus.send(alias_message)
+        if (message[&#39;sender&#39;] == self.conf[&#39;alias for&#39;] and
+                &#39;state&#39; in message):
+            if &#39;event&#39; in message and message[&#39;event&#39;] == &#39;changed&#39;:
+                alias_message[&#39;event&#39;] = &#39;changed&#39;
+            alias_message[&#39;state&#39;] = message[&#39;state&#39;]
+            await self.bus.send(alias_message)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                  &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                 MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                  &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                  &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                     &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                    MessageTemplate({&#39;sender&#39;:
+                                     {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                     &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                     &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                    MessageTemplate({&#39;sender&#39;:
+                                     {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                     &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        self.bus.register(self.name, &#39;StateAlias&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.state.StateAlias.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for StateAlias plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'alias for': name of aliased state.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.state.StateAlias.receive"><code class="name flex">
+<span>async def <span class="ident">receive</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Translate states from and commands to aliased state.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def receive(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Translate states from and commands to aliased state.&#34;&#34;&#34;
+    alias_message = Message(self.name)
+    if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+            &#39;command&#39; in message):
+        alias_message[&#39;target&#39;] = self.conf[&#39;alias for&#39;]
+        if message[&#39;command&#39;] == &#39;get state&#39;:
+            alias_message[&#39;command&#39;] = &#39;get state&#39;
+            await self.bus.send(alias_message)
+        elif (message[&#39;command&#39;] == &#39;set state&#39; and
+                &#39;new state&#39; in message):
+            alias_message[&#39;command&#39;] = &#39;set state&#39;
+            alias_message[&#39;new state&#39;] = message[&#39;new state&#39;]
+            await self.bus.send(alias_message)
+    if (message[&#39;sender&#39;] == self.conf[&#39;alias for&#39;] and
+            &#39;state&#39; in message):
+        if &#39;event&#39; in message and message[&#39;event&#39;] == &#39;changed&#39;:
+            alias_message[&#39;event&#39;] = &#39;changed&#39;
+        alias_message[&#39;state&#39;] = message[&#39;state&#39;]
+        await self.bus.send(alias_message)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.StateAlias.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    sends = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                              &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+             MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                              &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                              &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+             MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                              &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+             MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}}),
+                MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                                 &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                MessageTemplate({&#39;sender&#39;:
+                                 {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                 &#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                 &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                MessageTemplate({&#39;sender&#39;:
+                                 {&#39;const&#39;: self.conf[&#39;alias for&#39;]},
+                                 &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    self.bus.register(self.name, &#39;StateAlias&#39;,
+                      sends, receives, self.receive)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.StateAlias.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.state.AndState"><code class="flex name class">
+<span>class <span class="ident">AndState</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Implement conjunction of states.</p>
+<p>The "states" configuration key gets an array of states to be combined.
+An AndState plugin client reacts to "get state" commands and sends
+"changed" events when a change in one of the combined states leads to
+a change for the conjunction:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test State 1&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test State 2&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test AndState&quot;: {&quot;plugin&quot;: &quot;AndState&quot;,
+...                        &quot;states&quot;: [&quot;Test State 1&quot;, &quot;Test State 2&quot;]}},
+...     [{&quot;target&quot;: &quot;Test State 1&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State 2&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State 1&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: False},
+...      {&quot;target&quot;: &quot;Test AndState&quot;, &quot;command&quot;: &quot;get state&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State 1', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State 1'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State 1'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State 2', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State 2'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State 2'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test AndState', 'plugin': 'AndState',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test AndState'},
+                       'command': {'const': 'get state'}},
+                      {'sender': {'const': 'Test State 1'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State 2'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': 'test()', 'target': 'Test State 1',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State 2',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test AndState', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State 1',
+         'command': 'set state', 'new state': False}
+test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False}
+test(): {'sender': 'Test AndState', 'event': 'changed', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test AndState',
+         'command': 'get state'}
+test(): {'sender': 'Test AndState', 'state': False}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class AndState(BasePlugin):
+    &#34;&#34;&#34;Implement conjunction of states.
+
+    The &#34;states&#34; configuration key gets an array of states to be combined.
+    An AndState plugin client reacts to &#34;get state&#34; commands and sends
+    &#34;changed&#34; events when a change in one of the combined states leads to
+    a change for the conjunction:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State 1&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test State 2&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test AndState&#34;: {&#34;plugin&#34;: &#34;AndState&#34;,
+    ...                        &#34;states&#34;: [&#34;Test State 1&#34;, &#34;Test State 2&#34;]}},
+    ...     [{&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 2&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: False},
+    ...      {&#34;target&#34;: &#34;Test AndState&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 1&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 2&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test AndState&#39;, &#39;plugin&#39;: &#39;AndState&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test AndState&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 2&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 2&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test AndState&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test AndState&#39;, &#39;state&#39;: False}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;states&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;string&#39;}}},
+                   &#39;required&#39;: [&#39;states&#39;]}
+    &#34;&#34;&#34;Schema for AndState plugin configuration.
+
+    Required configuration key:
+
+    - &#39;states&#39;: list of names of combined states.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+            assert isinstance(message[&#39;sender&#39;], str)
+            assert isinstance(message[&#39;state&#39;], bool)
+            self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+            new_state = all(self.states.values())
+            if self.state != new_state:
+                self.state: bool = new_state
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+        self.states: Dict[str, bool] = {}
+        for state in self.conf[&#39;states&#39;]:
+            receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                             &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+            self.states[state] = False
+        self.state = all(self.states.values())
+        self.bus.register(self.name, &#39;AndState&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.state.AndState.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for AndState plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'states': list of names of combined states.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.state.AndState.receive"><code class="name flex">
+<span>async def <span class="ident">receive</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Process "get state" command and messages of combined states.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def receive(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+    if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+            &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+        await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+    if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+        assert isinstance(message[&#39;sender&#39;], str)
+        assert isinstance(message[&#39;state&#39;], bool)
+        self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+        new_state = all(self.states.values())
+        if self.state != new_state:
+            self.state: bool = new_state
+            await self.bus.send(Message(self.name,
+                                        {&#39;event&#39;: &#39;changed&#39;,
+                                         &#39;state&#39;: self.state}))</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.AndState.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                              &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+             MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+    self.states: Dict[str, bool] = {}
+    for state in self.conf[&#39;states&#39;]:
+        receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                         &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+        self.states[state] = False
+    self.state = all(self.states.values())
+    self.bus.register(self.name, &#39;AndState&#39;,
+                      sends, receives, self.receive)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.AndState.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.state.OrState"><code class="flex name class">
+<span>class <span class="ident">OrState</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Implement disjunction of states.</p>
+<p>The "states" configuration key gets an array of states to be combined.
+An OrState plugin client reacts to "get state" commands and sends
+"changed" events when a change in one of the combined states leads to
+a change for the disjunction:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import asyncio
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test State 1&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test State 2&quot;: {&quot;plugin&quot;: &quot;State&quot;},
+...      &quot;Test OrState&quot;: {&quot;plugin&quot;: &quot;OrState&quot;,
+...                       &quot;states&quot;: [&quot;Test State 1&quot;, &quot;Test State 2&quot;]}},
+...     [{&quot;target&quot;: &quot;Test State 1&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State 2&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: True},
+...      {&quot;target&quot;: &quot;Test State 1&quot;, &quot;command&quot;: &quot;set state&quot;,
+...       &quot;new state&quot;: False},
+...      {&quot;target&quot;: &quot;Test OrState&quot;, &quot;command&quot;: &quot;get state&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State 1', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State 1'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State 1'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test State 2', 'plugin': 'State',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test State 2'},
+                       'command': {'const': 'get state'}},
+                      {'target': {'const': 'Test State 2'},
+                       'command': {'const': 'set state'},
+                       'new state': {'type': 'boolean'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test OrState', 'plugin': 'OrState',
+         'sends': [{'event': {'const': 'changed'},
+                    'state': {'type': 'boolean'}},
+                   {'state': {'type': 'boolean'}}],
+         'receives': [{'target': {'const': 'Test OrState'},
+                       'command': {'const': 'get state'}},
+                      {'sender': {'const': 'Test State 1'},
+                       'state': {'type': 'boolean'}},
+                      {'sender': {'const': 'Test State 2'},
+                       'state': {'type': 'boolean'}}]}
+test(): {'sender': 'test()', 'target': 'Test State 1',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State 1', 'event': 'changed', 'state': True}
+test(): {'sender': 'Test OrState', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State 2',
+         'command': 'set state', 'new state': True}
+test(): {'sender': 'Test State 2', 'event': 'changed', 'state': True}
+test(): {'sender': 'test()', 'target': 'Test State 1',
+         'command': 'set state', 'new state': False}
+test(): {'sender': 'Test State 1', 'event': 'changed', 'state': False}
+test(): {'sender': 'test()', 'target': 'Test OrState',
+         'command': 'get state'}
+test(): {'sender': 'Test OrState', 'state': True}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class OrState(BasePlugin):
+    &#34;&#34;&#34;Implement disjunction of states.
+
+    The &#34;states&#34; configuration key gets an array of states to be combined.
+    An OrState plugin client reacts to &#34;get state&#34; commands and sends
+    &#34;changed&#34; events when a change in one of the combined states leads to
+    a change for the disjunction:
+    &gt;&gt;&gt; import asyncio
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test State 1&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test State 2&#34;: {&#34;plugin&#34;: &#34;State&#34;},
+    ...      &#34;Test OrState&#34;: {&#34;plugin&#34;: &#34;OrState&#34;,
+    ...                       &#34;states&#34;: [&#34;Test State 1&#34;, &#34;Test State 2&#34;]}},
+    ...     [{&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 2&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: True},
+    ...      {&#34;target&#34;: &#34;Test State 1&#34;, &#34;command&#34;: &#34;set state&#34;,
+    ...       &#34;new state&#34;: False},
+    ...      {&#34;target&#34;: &#34;Test OrState&#34;, &#34;command&#34;: &#34;get state&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 1&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test State 2&#39;, &#39;plugin&#39;: &#39;State&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set state&#39;},
+                           &#39;new state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test OrState&#39;, &#39;plugin&#39;: &#39;OrState&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                        &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                       {&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test OrState&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 1&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}},
+                          {&#39;sender&#39;: {&#39;const&#39;: &#39;Test State 2&#39;},
+                           &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 2&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;Test State 2&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: True}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test State 1&#39;,
+             &#39;command&#39;: &#39;set state&#39;, &#39;new state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;Test State 1&#39;, &#39;event&#39;: &#39;changed&#39;, &#39;state&#39;: False}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test OrState&#39;,
+             &#39;command&#39;: &#39;get state&#39;}
+    test(): {&#39;sender&#39;: &#39;Test OrState&#39;, &#39;state&#39;: True}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;states&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;string&#39;}}},
+                   &#39;required&#39;: [&#39;states&#39;]}
+    &#34;&#34;&#34;Schema for OrState plugin configuration.
+
+    Required configuration key:
+
+    - &#39;states&#39;: list of names of combined states.
+    &#34;&#34;&#34;
+
+    async def receive(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+        if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+                &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+            await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+        if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+            assert isinstance(message[&#39;sender&#39;], str)
+            assert isinstance(message[&#39;state&#39;], bool)
+            self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+            new_state = any(self.states.values())
+            if self.state != new_state:
+                self.state: bool = new_state
+                await self.bus.send(Message(self.name,
+                                            {&#39;event&#39;: &#39;changed&#39;,
+                                             &#39;state&#39;: self.state}))
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                                  &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+                 MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+        self.states: Dict[str, bool] = {}
+        for state in self.conf[&#39;states&#39;]:
+            receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                             &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+            self.states[state] = False
+        self.state = any(self.states.values())
+        self.bus.register(self.name, &#39;OrState&#39;,
+                          sends, receives, self.receive)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.state.OrState.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for OrState plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'states': list of names of combined states.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.state.OrState.receive"><code class="name flex">
+<span>async def <span class="ident">receive</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Process "get state" command and messages of combined states.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def receive(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Process &#34;get state&#34; command and messages of combined states.&#34;&#34;&#34;
+    if (&#39;target&#39; in message and message[&#39;target&#39;] == self.name and
+            &#39;command&#39; in message and message[&#39;command&#39;] == &#39;get state&#39;):
+        await self.bus.send(Message(self.name, {&#39;state&#39;: self.state}))
+    if &#39;state&#39; in message and message[&#39;sender&#39;] in self.conf[&#39;states&#39;]:
+        assert isinstance(message[&#39;sender&#39;], str)
+        assert isinstance(message[&#39;state&#39;], bool)
+        self.states[message[&#39;sender&#39;]] = message[&#39;state&#39;]
+        new_state = any(self.states.values())
+        if self.state != new_state:
+            self.state: bool = new_state
+            await self.bus.send(Message(self.name,
+                                        {&#39;event&#39;: &#39;changed&#39;,
+                                         &#39;state&#39;: self.state}))</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.OrState.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;changed&#39;},
+                              &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}),
+             MessageTemplate({&#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}})]
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;get state&#39;}})]
+    self.states: Dict[str, bool] = {}
+    for state in self.conf[&#39;states&#39;]:
+        receives.append(MessageTemplate({&#39;sender&#39;: {&#39;const&#39;: state},
+                                         &#39;state&#39;: {&#39;type&#39;: &#39;boolean&#39;}}))
+        self.states[state] = False
+    self.state = any(self.states.values())
+    self.bus.register(self.name, &#39;OrState&#39;,
+                      sends, receives, self.receive)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.state.OrState.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi_plugins" href="index.html">controlpi_plugins</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi_plugins.state.State" href="#controlpi_plugins.state.State">State</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.state.State.receive" href="#controlpi_plugins.state.State.receive">receive</a></code></li>
+<li><code><a title="controlpi_plugins.state.State.process_conf" href="#controlpi_plugins.state.State.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.state.State.run" href="#controlpi_plugins.state.State.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.state.State.CONF_SCHEMA" href="#controlpi_plugins.state.State.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.state.StateAlias" href="#controlpi_plugins.state.StateAlias">StateAlias</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.state.StateAlias.receive" href="#controlpi_plugins.state.StateAlias.receive">receive</a></code></li>
+<li><code><a title="controlpi_plugins.state.StateAlias.process_conf" href="#controlpi_plugins.state.StateAlias.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.state.StateAlias.run" href="#controlpi_plugins.state.StateAlias.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.state.StateAlias.CONF_SCHEMA" href="#controlpi_plugins.state.StateAlias.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.state.AndState" href="#controlpi_plugins.state.AndState">AndState</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.state.AndState.receive" href="#controlpi_plugins.state.AndState.receive">receive</a></code></li>
+<li><code><a title="controlpi_plugins.state.AndState.process_conf" href="#controlpi_plugins.state.AndState.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.state.AndState.run" href="#controlpi_plugins.state.AndState.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.state.AndState.CONF_SCHEMA" href="#controlpi_plugins.state.AndState.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.state.OrState" href="#controlpi_plugins.state.OrState">OrState</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.state.OrState.receive" href="#controlpi_plugins.state.OrState.receive">receive</a></code></li>
+<li><code><a title="controlpi_plugins.state.OrState.process_conf" href="#controlpi_plugins.state.OrState.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.state.OrState.run" href="#controlpi_plugins.state.OrState.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.state.OrState.CONF_SCHEMA" href="#controlpi_plugins.state.OrState.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi_plugins/util.html b/doc/controlpi_plugins/util.html
new file mode 100644 (file)
index 0000000..ca720bb
--- /dev/null
@@ -0,0 +1,1421 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi_plugins.util API documentation</title>
+<meta name="description" content="Provide utility plugins for all kinds of systems …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi_plugins.util</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide utility plugins for all kinds of systems.</p>
+<ul>
+<li>Log logs messages on stdout.</li>
+<li>Init sends list of messages on startup and on demand.</li>
+<li>Alias translates messages to an alias.</li>
+</ul>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Log&quot;: {&quot;plugin&quot;: &quot;Log&quot;,
+...                   &quot;filter&quot;: [{&quot;sender&quot;: {&quot;const&quot;: &quot;Test Alias&quot;}}]},
+...      &quot;Test Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;,
+...                    &quot;messages&quot;: [{&quot;id&quot;: 42, &quot;content&quot;: &quot;Test Message&quot;}]},
+...      &quot;Test Alias&quot;: {&quot;plugin&quot;: &quot;Alias&quot;,
+...                     &quot;from&quot;: {&quot;sender&quot;: {&quot;const&quot;: &quot;Test Init&quot;},
+...                              &quot;id&quot;: {&quot;const&quot;: 42}},
+...                     &quot;to&quot;: {&quot;id&quot;: &quot;translated&quot;}}}, []))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Log', 'plugin': 'Log',
+         'sends': [], 'receives': [{'sender': {'const': 'Test Alias'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Init', 'plugin': 'Init',
+         'sends': [{'id': {'const': 42},
+                    'content': {'const': 'Test Message'}}],
+         'receives': [{'target': {'const': 'Test Init'},
+                       'command': {'const': 'execute'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Alias', 'plugin': 'Alias',
+         'sends': [{'id': {'const': 'translated'}}],
+         'receives': [{'sender': {'const': 'Test Init'},
+                       'id': {'const': 42}}]}
+test(): {'sender': 'Test Init', 'id': 42,
+         'content': 'Test Message'}
+test(): {'sender': 'Test Alias', 'id': 'translated',
+         'content': 'Test Message'}
+Test Log: {'sender': 'Test Alias', 'id': 'translated',
+           'content': 'Test Message'}
+</code></pre>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide utility plugins for all kinds of systems.
+
+- Log logs messages on stdout.
+- Init sends list of messages on startup and on demand.
+- Alias translates messages to an alias.
+
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;,
+...                   &#34;filter&#34;: [{&#34;sender&#34;: {&#34;const&#34;: &#34;Test Alias&#34;}}]},
+...      &#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+...                    &#34;messages&#34;: [{&#34;id&#34;: 42, &#34;content&#34;: &#34;Test Message&#34;}]},
+...      &#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;,
+...                     &#34;from&#34;: {&#34;sender&#34;: {&#34;const&#34;: &#34;Test Init&#34;},
+...                              &#34;id&#34;: {&#34;const&#34;: 42}},
+...                     &#34;to&#34;: {&#34;id&#34;: &#34;translated&#34;}}}, []))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test Log&#39;, &#39;plugin&#39;: &#39;Log&#39;,
+         &#39;sends&#39;: [], &#39;receives&#39;: [{&#39;sender&#39;: {&#39;const&#39;: &#39;Test Alias&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test Init&#39;, &#39;plugin&#39;: &#39;Init&#39;,
+         &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42},
+                    &#39;content&#39;: {&#39;const&#39;: &#39;Test Message&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Init&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test Alias&#39;, &#39;plugin&#39;: &#39;Alias&#39;,
+         &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: &#39;translated&#39;}}],
+         &#39;receives&#39;: [{&#39;sender&#39;: {&#39;const&#39;: &#39;Test Init&#39;},
+                       &#39;id&#39;: {&#39;const&#39;: 42}}]}
+test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42,
+         &#39;content&#39;: &#39;Test Message&#39;}
+test(): {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+         &#39;content&#39;: &#39;Test Message&#39;}
+Test Log: {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+           &#39;content&#39;: &#39;Test Message&#39;}
+&#34;&#34;&#34;
+import asyncio
+
+from controlpi import BasePlugin, Message, MessageTemplate
+
+
+class Log(BasePlugin):
+    &#34;&#34;&#34;Log messages on stdout.
+
+    The &#34;filter&#34; configuration key gets a list of message templates defining
+    the messages that should be logged by the plugin instance.
+
+    In the following example the first and third message match the given
+    template and are logged by the instance &#34;Test Log&#34;, while the second
+    message does not match and is only logged by the test, but not by the
+    Log instance:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;,
+    ...                   &#34;filter&#34;: [{&#34;id&#34;: {&#34;const&#34;: 42}}]}},
+    ...     [{&#34;id&#34;: 42, &#34;message&#34;: &#34;Test Message&#34;},
+    ...      {&#34;id&#34;: 42.42, &#34;message&#34;: &#34;Second Message&#34;},
+    ...      {&#34;id&#34;: 42, &#34;message&#34;: &#34;Third Message&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Log&#39;, &#39;plugin&#39;: &#39;Log&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Test Message&#39;}
+    Test Log: {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42.42, &#39;message&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Third Message&#39;}
+    Test Log: {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Third Message&#39;}
+
+    The &#34;filter&#34; key is required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;}}, []))
+    &#39;filter&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;filter&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                   &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;filter&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Log&#39;}
+    Configuration for &#39;Test Log&#39; is not valid.
+
+    The &#34;filter&#34; key has to contain a list of message templates, i.e.,
+    JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;,
+    ...                   &#34;filter&#34;: [42]}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;filter&#39;][&#39;items&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;filter&#39;][0]:
+        42
+    Configuration for &#39;Test Log&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;filter&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                   &#39;required&#39;: [&#39;filter&#39;]}
+    &#34;&#34;&#34;Schema for Log plugin configuration.
+
+    Required configuration key:
+
+    - &#39;filter&#39;: list of message templates to be logged.
+    &#34;&#34;&#34;
+
+    async def log(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Log received message on stdout using own name as prefix.&#34;&#34;&#34;
+        print(f&#34;{self.name}: {message}&#34;)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.bus.register(self.name, &#39;Log&#39;, [], self.conf[&#39;filter&#39;], self.log)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class Init(BasePlugin):
+    &#34;&#34;&#34;Send list of messages on startup and on demand.
+
+    The &#34;messages&#34; configuration key gets a list of messages to be sent on
+    startup. The same list is sent in reaction to a message with
+    &#34;target&#34;: &lt;name&gt; and &#34;command&#34;: &#34;execute&#34;.
+
+    In the example, the two configured messages are sent twice, once at
+    startup and a second time in reaction to the &#34;execute&#34; command sent by
+    the test:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                    &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                                  &#34;content&#34;: &#34;Test Message&#34;},
+    ...                                 {&#34;id&#34;: 42.42,
+    ...                                  &#34;content&#34;: &#34;Second Message&#34;}]}},
+    ...     [{&#34;target&#34;: &#34;Test Init&#34;, &#34;command&#34;: &#34;execute&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Init&#39;, &#39;plugin&#39;: &#39;Init&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Test Message&#39;}},
+                       {&#39;id&#39;: {&#39;const&#39;: 42.42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Second Message&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Init&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Init&#39;, &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+
+    The &#34;messages&#34; key is required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;}}, []))
+    &#39;messages&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;messages&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                     &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;messages&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Init&#39;}
+    Configuration for &#39;Test Init&#39; is not valid.
+
+    The &#34;messages&#34; key has to contain a list of (partial) messages, i.e.,
+    JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                    &#34;messages&#34;: [42]}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;messages&#39;][&#39;items&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;messages&#39;][0]:
+        42
+    Configuration for &#39;Test Init&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;messages&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                               &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                   &#39;required&#39;: [&#39;messages&#39;]}
+    &#34;&#34;&#34;Schema for Init plugin configuration.
+
+    Required configuration key:
+
+    - &#39;messages&#39;: list of messages to be sent.
+    &#34;&#34;&#34;
+
+    async def execute(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Send configured messages.&#34;&#34;&#34;
+        for message in self.conf[&#39;messages&#39;]:
+            await self.bus.send(Message(self.name, message))
+            # Give immediate reactions to messages opportunity to happen:
+            await asyncio.sleep(0)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+        sends = [MessageTemplate.from_message(message)
+                 for message in self.conf[&#39;messages&#39;]]
+        self.bus.register(self.name, &#39;Init&#39;, sends, receives, self.execute)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Send configured messages on startup.&#34;&#34;&#34;
+        for message in self.conf[&#39;messages&#39;]:
+            await self.bus.send(Message(self.name, message))
+
+
+class Execute(BasePlugin):
+    &#34;&#34;&#34;Send configurable list of messages on demand.
+
+    An Execute plugin instance receives two kinds of commands.
+    The &#34;set messages&#34; command has a &#34;messages&#34; key with a list of (partial)
+    messages, which are sent by the Execute instance in reaction to an
+    &#34;execute&#34; command.
+
+    In the example, the first command sent by the test sets two messages,
+    which are then sent in reaction to the second command sent by the test:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Execute&#34;: {&#34;plugin&#34;: &#34;Execute&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test Execute&#34;, &#34;command&#34;: &#34;set messages&#34;,
+    ...       &#34;messages&#34;: [{&#34;id&#34;: 42, &#34;content&#34;: &#34;Test Message&#34;},
+    ...                    {&#34;id&#34;: 42.42, &#34;content&#34;: &#34;Second Message&#34;}]},
+    ...      {&#34;target&#34;: &#34;Test Execute&#34;, &#34;command&#34;: &#34;execute&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Execute&#39;, &#39;plugin&#39;: &#39;Execute&#39;,
+             &#39;sends&#39;: [{}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Execute&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set messages&#39;},
+                           &#39;messages&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                        &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test Execute&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Execute&#39;,
+             &#39;command&#39;: &#39;set messages&#39;,
+             &#39;messages&#39;: [{&#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;},
+                          {&#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Execute&#39;,
+             &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Execute&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Execute&#39;, &#39;id&#39;: 42.42,
+             &#39;content&#39;: &#39;Second Message&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for Execute plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def execute(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Set or send configured messages.&#34;&#34;&#34;
+        if message[&#39;command&#39;] == &#39;set messages&#39;:
+            assert isinstance(message[&#39;messages&#39;], list)
+            self.messages = list(message[&#39;messages&#39;])
+        elif message[&#39;command&#39;] == &#39;execute&#39;:
+            for message in self.messages:
+                await self.bus.send(Message(self.name, message))
+                # Give immediate reactions to messages opportunity to happen:
+                await asyncio.sleep(0)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.messages = []
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set messages&#39;},
+                                     &#39;messages&#39;:
+                                     {&#39;type&#39;: &#39;array&#39;,
+                                      &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+        sends = [MessageTemplate()]
+        self.bus.register(self.name, &#39;Execute&#39;, sends, receives, self.execute)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class Alias(BasePlugin):
+    &#34;&#34;&#34;Translate messages to an alias.
+
+    The &#34;from&#34; configuration key gets a message template and the
+    configuration key &#34;to&#34; a (partial) message. All messages matching the
+    template are received by the Alias instance and a message translated by
+    removing the keys of the &#34;from&#34; template and adding the keys and values
+    of the &#34;to&#34; message is sent. Keys that are neither in &#34;from&#34; nor in &#34;to&#34;
+    are retained.
+
+    In the example, the two messages sent by the test are translated by the
+    Alias instance and the translated messages are sent by it preserving
+    the &#34;content&#34; keys:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;,
+    ...                     &#34;from&#34;: {&#34;id&#34;: {&#34;const&#34;: 42}},
+    ...                     &#34;to&#34;: {&#34;id&#34;: &#34;translated&#34;}}},
+    ...     [{&#34;id&#34;: 42, &#34;content&#34;: &#34;Test Message&#34;},
+    ...      {&#34;id&#34;: 42, &#34;content&#34;: &#34;Second Message&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Alias&#39;, &#39;plugin&#39;: &#39;Alias&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: &#39;translated&#39;}}],
+             &#39;receives&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+             &#39;content&#39;: &#39;Second Message&#39;}
+
+    The &#34;from&#34; and &#34;to&#34; keys are required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;}}, []))
+    &#39;from&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;}, &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+         &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Alias&#39;}
+    &#39;to&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;}, &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+         &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Alias&#39;}
+    Configuration for &#39;Test Alias&#39; is not valid.
+
+    The &#34;from&#34; key has to contain a message template and the &#34;to&#34; key a
+    (partial) message, i.e., both have to be JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;,
+    ...                     &#34;from&#34;: 42,
+    ...                     &#34;to&#34;: 42}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;from&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;from&#39;]:
+        42
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;to&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;to&#39;]:
+        42
+    Configuration for &#39;Test Alias&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                  &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+                   &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &#34;&#34;&#34;Schema for Alias plugin configuration.
+
+    Required configuration keys:
+
+    - &#39;from&#39;: template of messages to be translated.
+    - &#39;to&#39;: translated message to be sent.
+    &#34;&#34;&#34;
+
+    async def alias(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Translate and send message.&#34;&#34;&#34;
+        alias_message = Message(self.name)
+        alias_message.update(self.conf[&#39;to&#39;])
+        for key in message:
+            if key != &#39;sender&#39; and key not in self.conf[&#39;from&#39;]:
+                alias_message[key] = message[key]
+        await self.bus.send(alias_message)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.bus.register(self.name, &#39;Alias&#39;,
+                          [MessageTemplate.from_message(self.conf[&#39;to&#39;])],
+                          [self.conf[&#39;from&#39;]],
+                          self.alias)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi_plugins.util.Log"><code class="flex name class">
+<span>class <span class="ident">Log</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Log messages on stdout.</p>
+<p>The "filter" configuration key gets a list of message templates defining
+the messages that should be logged by the plugin instance.</p>
+<p>In the following example the first and third message match the given
+template and are logged by the instance "Test Log", while the second
+message does not match and is only logged by the test, but not by the
+Log instance:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Log&quot;: {&quot;plugin&quot;: &quot;Log&quot;,
+...                   &quot;filter&quot;: [{&quot;id&quot;: {&quot;const&quot;: 42}}]}},
+...     [{&quot;id&quot;: 42, &quot;message&quot;: &quot;Test Message&quot;},
+...      {&quot;id&quot;: 42.42, &quot;message&quot;: &quot;Second Message&quot;},
+...      {&quot;id&quot;: 42, &quot;message&quot;: &quot;Third Message&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Log', 'plugin': 'Log',
+         'sends': [], 'receives': [{'id': {'const': 42}}]}
+test(): {'sender': 'test()', 'id': 42, 'message': 'Test Message'}
+Test Log: {'sender': 'test()', 'id': 42, 'message': 'Test Message'}
+test(): {'sender': 'test()', 'id': 42.42, 'message': 'Second Message'}
+test(): {'sender': 'test()', 'id': 42, 'message': 'Third Message'}
+Test Log: {'sender': 'test()', 'id': 42, 'message': 'Third Message'}
+</code></pre>
+<p>The "filter" key is required:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Log&quot;: {&quot;plugin&quot;: &quot;Log&quot;}}, []))
+'filter' is a required property
+&lt;BLANKLINE&gt;
+Failed validating 'required' in schema:
+    {'properties': {'filter': {'items': {'type': 'object'},
+                               'type': 'array'}},
+     'required': ['filter']}
+&lt;BLANKLINE&gt;
+On instance:
+    {'plugin': 'Log'}
+Configuration for 'Test Log' is not valid.
+</code></pre>
+<p>The "filter" key has to contain a list of message templates, i.e.,
+JSON objects:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Log&quot;: {&quot;plugin&quot;: &quot;Log&quot;,
+...                   &quot;filter&quot;: [42]}}, []))
+42 is not of type 'object'
+&lt;BLANKLINE&gt;
+Failed validating 'type' in schema['properties']['filter']['items']:
+    {'type': 'object'}
+&lt;BLANKLINE&gt;
+On instance['filter'][0]:
+    42
+Configuration for 'Test Log' is not valid.
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Log(BasePlugin):
+    &#34;&#34;&#34;Log messages on stdout.
+
+    The &#34;filter&#34; configuration key gets a list of message templates defining
+    the messages that should be logged by the plugin instance.
+
+    In the following example the first and third message match the given
+    template and are logged by the instance &#34;Test Log&#34;, while the second
+    message does not match and is only logged by the test, but not by the
+    Log instance:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;,
+    ...                   &#34;filter&#34;: [{&#34;id&#34;: {&#34;const&#34;: 42}}]}},
+    ...     [{&#34;id&#34;: 42, &#34;message&#34;: &#34;Test Message&#34;},
+    ...      {&#34;id&#34;: 42.42, &#34;message&#34;: &#34;Second Message&#34;},
+    ...      {&#34;id&#34;: 42, &#34;message&#34;: &#34;Third Message&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Log&#39;, &#39;plugin&#39;: &#39;Log&#39;,
+             &#39;sends&#39;: [], &#39;receives&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Test Message&#39;}
+    Test Log: {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42.42, &#39;message&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Third Message&#39;}
+    Test Log: {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42, &#39;message&#39;: &#39;Third Message&#39;}
+
+    The &#34;filter&#34; key is required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;}}, []))
+    &#39;filter&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;filter&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                   &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;filter&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Log&#39;}
+    Configuration for &#39;Test Log&#39; is not valid.
+
+    The &#34;filter&#34; key has to contain a list of message templates, i.e.,
+    JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Log&#34;: {&#34;plugin&#34;: &#34;Log&#34;,
+    ...                   &#34;filter&#34;: [42]}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;filter&#39;][&#39;items&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;filter&#39;][0]:
+        42
+    Configuration for &#39;Test Log&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;filter&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                             &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                   &#39;required&#39;: [&#39;filter&#39;]}
+    &#34;&#34;&#34;Schema for Log plugin configuration.
+
+    Required configuration key:
+
+    - &#39;filter&#39;: list of message templates to be logged.
+    &#34;&#34;&#34;
+
+    async def log(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Log received message on stdout using own name as prefix.&#34;&#34;&#34;
+        print(f&#34;{self.name}: {message}&#34;)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.bus.register(self.name, &#39;Log&#39;, [], self.conf[&#39;filter&#39;], self.log)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.util.Log.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for Log plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'filter': list of message templates to be logged.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.util.Log.log"><code class="name flex">
+<span>async def <span class="ident">log</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Log received message on stdout using own name as prefix.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def log(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Log received message on stdout using own name as prefix.&#34;&#34;&#34;
+    print(f&#34;{self.name}: {message}&#34;)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Log.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    self.bus.register(self.name, &#39;Log&#39;, [], self.conf[&#39;filter&#39;], self.log)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Log.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.util.Init"><code class="flex name class">
+<span>class <span class="ident">Init</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Send list of messages on startup and on demand.</p>
+<p>The "messages" configuration key gets a list of messages to be sent on
+startup. The same list is sent in reaction to a message with
+"target": <name> and "command": "execute".</p>
+<p>In the example, the two configured messages are sent twice, once at
+startup and a second time in reaction to the "execute" command sent by
+the test:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;,
+...                    &quot;messages&quot;: [{&quot;id&quot;: 42,
+...                                  &quot;content&quot;: &quot;Test Message&quot;},
+...                                 {&quot;id&quot;: 42.42,
+...                                  &quot;content&quot;: &quot;Second Message&quot;}]}},
+...     [{&quot;target&quot;: &quot;Test Init&quot;, &quot;command&quot;: &quot;execute&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Init', 'plugin': 'Init',
+         'sends': [{'id': {'const': 42},
+                    'content': {'const': 'Test Message'}},
+                   {'id': {'const': 42.42},
+                    'content': {'const': 'Second Message'}}],
+         'receives': [{'target': {'const': 'Test Init'},
+                       'command': {'const': 'execute'}}]}
+test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'}
+test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'}
+test(): {'sender': 'test()', 'target': 'Test Init', 'command': 'execute'}
+test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'}
+test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'}
+</code></pre>
+<p>The "messages" key is required:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;}}, []))
+'messages' is a required property
+&lt;BLANKLINE&gt;
+Failed validating 'required' in schema:
+    {'properties': {'messages': {'items': {'type': 'object'},
+                                 'type': 'array'}},
+     'required': ['messages']}
+&lt;BLANKLINE&gt;
+On instance:
+    {'plugin': 'Init'}
+Configuration for 'Test Init' is not valid.
+</code></pre>
+<p>The "messages" key has to contain a list of (partial) messages, i.e.,
+JSON objects:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Init&quot;: {&quot;plugin&quot;: &quot;Init&quot;,
+...                    &quot;messages&quot;: [42]}}, []))
+42 is not of type 'object'
+&lt;BLANKLINE&gt;
+Failed validating 'type' in schema['properties']['messages']['items']:
+    {'type': 'object'}
+&lt;BLANKLINE&gt;
+On instance['messages'][0]:
+    42
+Configuration for 'Test Init' is not valid.
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Init(BasePlugin):
+    &#34;&#34;&#34;Send list of messages on startup and on demand.
+
+    The &#34;messages&#34; configuration key gets a list of messages to be sent on
+    startup. The same list is sent in reaction to a message with
+    &#34;target&#34;: &lt;name&gt; and &#34;command&#34;: &#34;execute&#34;.
+
+    In the example, the two configured messages are sent twice, once at
+    startup and a second time in reaction to the &#34;execute&#34; command sent by
+    the test:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                    &#34;messages&#34;: [{&#34;id&#34;: 42,
+    ...                                  &#34;content&#34;: &#34;Test Message&#34;},
+    ...                                 {&#34;id&#34;: 42.42,
+    ...                                  &#34;content&#34;: &#34;Second Message&#34;}]}},
+    ...     [{&#34;target&#34;: &#34;Test Init&#34;, &#34;command&#34;: &#34;execute&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Init&#39;, &#39;plugin&#39;: &#39;Init&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Test Message&#39;}},
+                       {&#39;id&#39;: {&#39;const&#39;: 42.42},
+                        &#39;content&#39;: {&#39;const&#39;: &#39;Second Message&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Init&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Init&#39;, &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Init&#39;, &#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}
+
+    The &#34;messages&#34; key is required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;}}, []))
+    &#39;messages&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;messages&#39;: {&#39;items&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                     &#39;type&#39;: &#39;array&#39;}},
+         &#39;required&#39;: [&#39;messages&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Init&#39;}
+    Configuration for &#39;Test Init&#39; is not valid.
+
+    The &#34;messages&#34; key has to contain a list of (partial) messages, i.e.,
+    JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Init&#34;: {&#34;plugin&#34;: &#34;Init&#34;,
+    ...                    &#34;messages&#34;: [42]}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;messages&#39;][&#39;items&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;messages&#39;][0]:
+        42
+    Configuration for &#39;Test Init&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;messages&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                               &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                   &#39;required&#39;: [&#39;messages&#39;]}
+    &#34;&#34;&#34;Schema for Init plugin configuration.
+
+    Required configuration key:
+
+    - &#39;messages&#39;: list of messages to be sent.
+    &#34;&#34;&#34;
+
+    async def execute(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Send configured messages.&#34;&#34;&#34;
+        for message in self.conf[&#39;messages&#39;]:
+            await self.bus.send(Message(self.name, message))
+            # Give immediate reactions to messages opportunity to happen:
+            await asyncio.sleep(0)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+        sends = [MessageTemplate.from_message(message)
+                 for message in self.conf[&#39;messages&#39;]]
+        self.bus.register(self.name, &#39;Init&#39;, sends, receives, self.execute)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Send configured messages on startup.&#34;&#34;&#34;
+        for message in self.conf[&#39;messages&#39;]:
+            await self.bus.send(Message(self.name, message))</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.util.Init.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for Init plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'messages': list of messages to be sent.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.util.Init.execute"><code class="name flex">
+<span>async def <span class="ident">execute</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Send configured messages.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def execute(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Send configured messages.&#34;&#34;&#34;
+    for message in self.conf[&#39;messages&#39;]:
+        await self.bus.send(Message(self.name, message))
+        # Give immediate reactions to messages opportunity to happen:
+        await asyncio.sleep(0)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Init.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+    sends = [MessageTemplate.from_message(message)
+             for message in self.conf[&#39;messages&#39;]]
+    self.bus.register(self.name, &#39;Init&#39;, sends, receives, self.execute)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Init.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Send configured messages on startup.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Send configured messages on startup.&#34;&#34;&#34;
+    for message in self.conf[&#39;messages&#39;]:
+        await self.bus.send(Message(self.name, message))</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.util.Execute"><code class="flex name class">
+<span>class <span class="ident">Execute</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Send configurable list of messages on demand.</p>
+<p>An Execute plugin instance receives two kinds of commands.
+The "set messages" command has a "messages" key with a list of (partial)
+messages, which are sent by the Execute instance in reaction to an
+"execute" command.</p>
+<p>In the example, the first command sent by the test sets two messages,
+which are then sent in reaction to the second command sent by the test:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Execute&quot;: {&quot;plugin&quot;: &quot;Execute&quot;}},
+...     [{&quot;target&quot;: &quot;Test Execute&quot;, &quot;command&quot;: &quot;set messages&quot;,
+...       &quot;messages&quot;: [{&quot;id&quot;: 42, &quot;content&quot;: &quot;Test Message&quot;},
+...                    {&quot;id&quot;: 42.42, &quot;content&quot;: &quot;Second Message&quot;}]},
+...      {&quot;target&quot;: &quot;Test Execute&quot;, &quot;command&quot;: &quot;execute&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Execute', 'plugin': 'Execute',
+         'sends': [{}],
+         'receives': [{'target': {'const': 'Test Execute'},
+                       'command': {'const': 'set messages'},
+                       'messages': {'type': 'array',
+                                    'items': {'type': 'object'}}},
+                      {'target': {'const': 'Test Execute'},
+                       'command': {'const': 'execute'}}]}
+test(): {'sender': 'test()', 'target': 'Test Execute',
+         'command': 'set messages',
+         'messages': [{'id': 42, 'content': 'Test Message'},
+                      {'id': 42.42, 'content': 'Second Message'}]}
+test(): {'sender': 'test()', 'target': 'Test Execute',
+         'command': 'execute'}
+test(): {'sender': 'Test Execute', 'id': 42,
+         'content': 'Test Message'}
+test(): {'sender': 'Test Execute', 'id': 42.42,
+         'content': 'Second Message'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Execute(BasePlugin):
+    &#34;&#34;&#34;Send configurable list of messages on demand.
+
+    An Execute plugin instance receives two kinds of commands.
+    The &#34;set messages&#34; command has a &#34;messages&#34; key with a list of (partial)
+    messages, which are sent by the Execute instance in reaction to an
+    &#34;execute&#34; command.
+
+    In the example, the first command sent by the test sets two messages,
+    which are then sent in reaction to the second command sent by the test:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Execute&#34;: {&#34;plugin&#34;: &#34;Execute&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test Execute&#34;, &#34;command&#34;: &#34;set messages&#34;,
+    ...       &#34;messages&#34;: [{&#34;id&#34;: 42, &#34;content&#34;: &#34;Test Message&#34;},
+    ...                    {&#34;id&#34;: 42.42, &#34;content&#34;: &#34;Second Message&#34;}]},
+    ...      {&#34;target&#34;: &#34;Test Execute&#34;, &#34;command&#34;: &#34;execute&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Execute&#39;, &#39;plugin&#39;: &#39;Execute&#39;,
+             &#39;sends&#39;: [{}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Execute&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;set messages&#39;},
+                           &#39;messages&#39;: {&#39;type&#39;: &#39;array&#39;,
+                                        &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}},
+                          {&#39;target&#39;: {&#39;const&#39;: &#39;Test Execute&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Execute&#39;,
+             &#39;command&#39;: &#39;set messages&#39;,
+             &#39;messages&#39;: [{&#39;id&#39;: 42, &#39;content&#39;: &#39;Test Message&#39;},
+                          {&#39;id&#39;: 42.42, &#39;content&#39;: &#39;Second Message&#39;}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Execute&#39;,
+             &#39;command&#39;: &#39;execute&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Execute&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Execute&#39;, &#39;id&#39;: 42.42,
+             &#39;content&#39;: &#39;Second Message&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for Execute plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def execute(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Set or send configured messages.&#34;&#34;&#34;
+        if message[&#39;command&#39;] == &#39;set messages&#39;:
+            assert isinstance(message[&#39;messages&#39;], list)
+            self.messages = list(message[&#39;messages&#39;])
+        elif message[&#39;command&#39;] == &#39;execute&#39;:
+            for message in self.messages:
+                await self.bus.send(Message(self.name, message))
+                # Give immediate reactions to messages opportunity to happen:
+                await asyncio.sleep(0)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.messages = []
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;set messages&#39;},
+                                     &#39;messages&#39;:
+                                     {&#39;type&#39;: &#39;array&#39;,
+                                      &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}}),
+                    MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+        sends = [MessageTemplate()]
+        self.bus.register(self.name, &#39;Execute&#39;, sends, receives, self.execute)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.util.Execute.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for Execute plugin configuration.</p>
+<p>There are no required or optional configuration keys.</p></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.util.Execute.execute"><code class="name flex">
+<span>async def <span class="ident">execute</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Set or send configured messages.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def execute(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Set or send configured messages.&#34;&#34;&#34;
+    if message[&#39;command&#39;] == &#39;set messages&#39;:
+        assert isinstance(message[&#39;messages&#39;], list)
+        self.messages = list(message[&#39;messages&#39;])
+    elif message[&#39;command&#39;] == &#39;execute&#39;:
+        for message in self.messages:
+            await self.bus.send(Message(self.name, message))
+            # Give immediate reactions to messages opportunity to happen:
+            await asyncio.sleep(0)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Execute.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    self.messages = []
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;set messages&#39;},
+                                 &#39;messages&#39;:
+                                 {&#39;type&#39;: &#39;array&#39;,
+                                  &#39;items&#39;: {&#39;type&#39;: &#39;object&#39;}}}),
+                MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;execute&#39;}})]
+    sends = [MessageTemplate()]
+    self.bus.register(self.name, &#39;Execute&#39;, sends, receives, self.execute)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Execute.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.util.Alias"><code class="flex name class">
+<span>class <span class="ident">Alias</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Translate messages to an alias.</p>
+<p>The "from" configuration key gets a message template and the
+configuration key "to" a (partial) message. All messages matching the
+template are received by the Alias instance and a message translated by
+removing the keys of the "from" template and adding the keys and values
+of the "to" message is sent. Keys that are neither in "from" nor in "to"
+are retained.</p>
+<p>In the example, the two messages sent by the test are translated by the
+Alias instance and the translated messages are sent by it preserving
+the "content" keys:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Alias&quot;: {&quot;plugin&quot;: &quot;Alias&quot;,
+...                     &quot;from&quot;: {&quot;id&quot;: {&quot;const&quot;: 42}},
+...                     &quot;to&quot;: {&quot;id&quot;: &quot;translated&quot;}}},
+...     [{&quot;id&quot;: 42, &quot;content&quot;: &quot;Test Message&quot;},
+...      {&quot;id&quot;: 42, &quot;content&quot;: &quot;Second Message&quot;}]))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Alias', 'plugin': 'Alias',
+         'sends': [{'id': {'const': 'translated'}}],
+         'receives': [{'id': {'const': 42}}]}
+test(): {'sender': 'test()', 'id': 42,
+         'content': 'Test Message'}
+test(): {'sender': 'Test Alias', 'id': 'translated',
+         'content': 'Test Message'}
+test(): {'sender': 'test()', 'id': 42,
+         'content': 'Second Message'}
+test(): {'sender': 'Test Alias', 'id': 'translated',
+         'content': 'Second Message'}
+</code></pre>
+<p>The "from" and "to" keys are required:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Alias&quot;: {&quot;plugin&quot;: &quot;Alias&quot;}}, []))
+'from' is a required property
+&lt;BLANKLINE&gt;
+Failed validating 'required' in schema:
+    {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+     'required': ['from', 'to']}
+&lt;BLANKLINE&gt;
+On instance:
+    {'plugin': 'Alias'}
+'to' is a required property
+&lt;BLANKLINE&gt;
+Failed validating 'required' in schema:
+    {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+     'required': ['from', 'to']}
+&lt;BLANKLINE&gt;
+On instance:
+    {'plugin': 'Alias'}
+Configuration for 'Test Alias' is not valid.
+</code></pre>
+<p>The "from" key has to contain a message template and the "to" key a
+(partial) message, i.e., both have to be JSON objects:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Alias&quot;: {&quot;plugin&quot;: &quot;Alias&quot;,
+...                     &quot;from&quot;: 42,
+...                     &quot;to&quot;: 42}}, []))
+42 is not of type 'object'
+&lt;BLANKLINE&gt;
+Failed validating 'type' in schema['properties']['from']:
+    {'type': 'object'}
+&lt;BLANKLINE&gt;
+On instance['from']:
+    42
+42 is not of type 'object'
+&lt;BLANKLINE&gt;
+Failed validating 'type' in schema['properties']['to']:
+    {'type': 'object'}
+&lt;BLANKLINE&gt;
+On instance['to']:
+    42
+Configuration for 'Test Alias' is not valid.
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Alias(BasePlugin):
+    &#34;&#34;&#34;Translate messages to an alias.
+
+    The &#34;from&#34; configuration key gets a message template and the
+    configuration key &#34;to&#34; a (partial) message. All messages matching the
+    template are received by the Alias instance and a message translated by
+    removing the keys of the &#34;from&#34; template and adding the keys and values
+    of the &#34;to&#34; message is sent. Keys that are neither in &#34;from&#34; nor in &#34;to&#34;
+    are retained.
+
+    In the example, the two messages sent by the test are translated by the
+    Alias instance and the translated messages are sent by it preserving
+    the &#34;content&#34; keys:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;,
+    ...                     &#34;from&#34;: {&#34;id&#34;: {&#34;const&#34;: 42}},
+    ...                     &#34;to&#34;: {&#34;id&#34;: &#34;translated&#34;}}},
+    ...     [{&#34;id&#34;: 42, &#34;content&#34;: &#34;Test Message&#34;},
+    ...      {&#34;id&#34;: 42, &#34;content&#34;: &#34;Second Message&#34;}]))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test Alias&#39;, &#39;plugin&#39;: &#39;Alias&#39;,
+             &#39;sends&#39;: [{&#39;id&#39;: {&#39;const&#39;: &#39;translated&#39;}}],
+             &#39;receives&#39;: [{&#39;id&#39;: {&#39;const&#39;: 42}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+             &#39;content&#39;: &#39;Test Message&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;id&#39;: 42,
+             &#39;content&#39;: &#39;Second Message&#39;}
+    test(): {&#39;sender&#39;: &#39;Test Alias&#39;, &#39;id&#39;: &#39;translated&#39;,
+             &#39;content&#39;: &#39;Second Message&#39;}
+
+    The &#34;from&#34; and &#34;to&#34; keys are required:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;}}, []))
+    &#39;from&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;}, &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+         &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Alias&#39;}
+    &#39;to&#39; is a required property
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;required&#39; in schema:
+        {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;}, &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+         &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &lt;BLANKLINE&gt;
+    On instance:
+        {&#39;plugin&#39;: &#39;Alias&#39;}
+    Configuration for &#39;Test Alias&#39; is not valid.
+
+    The &#34;from&#34; key has to contain a message template and the &#34;to&#34; key a
+    (partial) message, i.e., both have to be JSON objects:
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test Alias&#34;: {&#34;plugin&#34;: &#34;Alias&#34;,
+    ...                     &#34;from&#34;: 42,
+    ...                     &#34;to&#34;: 42}}, []))
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;from&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;from&#39;]:
+        42
+    42 is not of type &#39;object&#39;
+    &lt;BLANKLINE&gt;
+    Failed validating &#39;type&#39; in schema[&#39;properties&#39;][&#39;to&#39;]:
+        {&#39;type&#39;: &#39;object&#39;}
+    &lt;BLANKLINE&gt;
+    On instance[&#39;to&#39;]:
+        42
+    Configuration for &#39;Test Alias&#39; is not valid.
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;from&#39;: {&#39;type&#39;: &#39;object&#39;},
+                                  &#39;to&#39;: {&#39;type&#39;: &#39;object&#39;}},
+                   &#39;required&#39;: [&#39;from&#39;, &#39;to&#39;]}
+    &#34;&#34;&#34;Schema for Alias plugin configuration.
+
+    Required configuration keys:
+
+    - &#39;from&#39;: template of messages to be translated.
+    - &#39;to&#39;: translated message to be sent.
+    &#34;&#34;&#34;
+
+    async def alias(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Translate and send message.&#34;&#34;&#34;
+        alias_message = Message(self.name)
+        alias_message.update(self.conf[&#39;to&#39;])
+        for key in message:
+            if key != &#39;sender&#39; and key not in self.conf[&#39;from&#39;]:
+                alias_message[key] = message[key]
+        await self.bus.send(alias_message)
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        self.bus.register(self.name, &#39;Alias&#39;,
+                          [MessageTemplate.from_message(self.conf[&#39;to&#39;])],
+                          [self.conf[&#39;from&#39;]],
+                          self.alias)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.util.Alias.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for Alias plugin configuration.</p>
+<p>Required configuration keys:</p>
+<ul>
+<li>'from': template of messages to be translated.</li>
+<li>'to': translated message to be sent.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.util.Alias.alias"><code class="name flex">
+<span>async def <span class="ident">alias</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Translate and send message.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def alias(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Translate and send message.&#34;&#34;&#34;
+    alias_message = Message(self.name)
+    alias_message.update(self.conf[&#39;to&#39;])
+    for key in message:
+        if key != &#39;sender&#39; and key not in self.conf[&#39;from&#39;]:
+            alias_message[key] = message[key]
+    await self.bus.send(alias_message)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Alias.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    self.bus.register(self.name, &#39;Alias&#39;,
+                      [MessageTemplate.from_message(self.conf[&#39;to&#39;])],
+                      [self.conf[&#39;from&#39;]],
+                      self.alias)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.util.Alias.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi_plugins" href="index.html">controlpi_plugins</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi_plugins.util.Log" href="#controlpi_plugins.util.Log">Log</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.util.Log.log" href="#controlpi_plugins.util.Log.log">log</a></code></li>
+<li><code><a title="controlpi_plugins.util.Log.process_conf" href="#controlpi_plugins.util.Log.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.util.Log.run" href="#controlpi_plugins.util.Log.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.util.Log.CONF_SCHEMA" href="#controlpi_plugins.util.Log.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.util.Init" href="#controlpi_plugins.util.Init">Init</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.util.Init.execute" href="#controlpi_plugins.util.Init.execute">execute</a></code></li>
+<li><code><a title="controlpi_plugins.util.Init.process_conf" href="#controlpi_plugins.util.Init.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.util.Init.run" href="#controlpi_plugins.util.Init.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.util.Init.CONF_SCHEMA" href="#controlpi_plugins.util.Init.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.util.Execute" href="#controlpi_plugins.util.Execute">Execute</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.util.Execute.execute" href="#controlpi_plugins.util.Execute.execute">execute</a></code></li>
+<li><code><a title="controlpi_plugins.util.Execute.process_conf" href="#controlpi_plugins.util.Execute.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.util.Execute.run" href="#controlpi_plugins.util.Execute.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.util.Execute.CONF_SCHEMA" href="#controlpi_plugins.util.Execute.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.util.Alias" href="#controlpi_plugins.util.Alias">Alias</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.util.Alias.alias" href="#controlpi_plugins.util.Alias.alias">alias</a></code></li>
+<li><code><a title="controlpi_plugins.util.Alias.process_conf" href="#controlpi_plugins.util.Alias.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.util.Alias.run" href="#controlpi_plugins.util.Alias.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.util.Alias.CONF_SCHEMA" href="#controlpi_plugins.util.Alias.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/doc/controlpi_plugins/wait.html b/doc/controlpi_plugins/wait.html
new file mode 100644 (file)
index 0000000..a5230a5
--- /dev/null
@@ -0,0 +1,604 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+<meta name="generator" content="pdoc 0.9.2" />
+<title>controlpi_plugins.wait API documentation</title>
+<meta name="description" content="Provide waiting/sleeping plugins for all kinds of systems …" />
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin>
+<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin>
+<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin>
+<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
+<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
+<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
+<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script>
+<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script>
+</head>
+<body>
+<main>
+<article id="content">
+<header>
+<h1 class="title">Module <code>controlpi_plugins.wait</code></h1>
+</header>
+<section id="section-intro">
+<p>Provide waiting/sleeping plugins for all kinds of systems.</p>
+<ul>
+<li>Wait waits for time defined in configuration and sends "finished" event.</li>
+<li>GenericWait waits for time defined in "wait" command and sends "finished"
+event with "id" string defined in "wait" command.</li>
+</ul>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test Wait&quot;: {&quot;plugin&quot;: &quot;Wait&quot;, &quot;seconds&quot;: 0.01},
+...      &quot;Test GenericWait&quot;: {&quot;plugin&quot;: &quot;GenericWait&quot;}},
+...     [{&quot;target&quot;: &quot;Test GenericWait&quot;, &quot;command&quot;: &quot;wait&quot;,
+...       &quot;seconds&quot;: 0.02, &quot;id&quot;: &quot;Long Wait&quot;},
+...      {&quot;target&quot;: &quot;Test Wait&quot;, &quot;command&quot;: &quot;wait&quot;}], 0.025))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test Wait', 'plugin': 'Wait',
+         'sends': [{'event': {'const': 'finished'}}],
+         'receives': [{'target': {'const': 'Test Wait'},
+                       'command': {'const': 'wait'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test GenericWait', 'plugin': 'GenericWait',
+         'sends': [{'event': {'const': 'finished'},
+                    'id': {'type': 'string'}}],
+         'receives': [{'target': {'const': 'Test GenericWait'},
+                       'command': {'const': 'wait'},
+                       'seconds': {'type': 'number'},
+                       'id': {'type': 'string'}}]}
+test(): {'sender': 'test()', 'target': 'Test GenericWait',
+         'command': 'wait', 'seconds': 0.02, 'id': 'Long Wait'}
+test(): {'sender': 'test()', 'target': 'Test Wait', 'command': 'wait'}
+test(): {'sender': 'Test Wait', 'event': 'finished'}
+test(): {'sender': 'Test GenericWait', 'event': 'finished',
+         'id': 'Long Wait'}
+</code></pre>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">&#34;&#34;&#34;Provide waiting/sleeping plugins for all kinds of systems.
+
+- Wait waits for time defined in configuration and sends &#34;finished&#34; event.
+- GenericWait waits for time defined in &#34;wait&#34; command and sends &#34;finished&#34;
+  event with &#34;id&#34; string defined in &#34;wait&#34; command.
+
+&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&#34;Test Wait&#34;: {&#34;plugin&#34;: &#34;Wait&#34;, &#34;seconds&#34;: 0.01},
+...      &#34;Test GenericWait&#34;: {&#34;plugin&#34;: &#34;GenericWait&#34;}},
+...     [{&#34;target&#34;: &#34;Test GenericWait&#34;, &#34;command&#34;: &#34;wait&#34;,
+...       &#34;seconds&#34;: 0.02, &#34;id&#34;: &#34;Long Wait&#34;},
+...      {&#34;target&#34;: &#34;Test Wait&#34;, &#34;command&#34;: &#34;wait&#34;}], 0.025))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test Wait&#39;, &#39;plugin&#39;: &#39;Wait&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test Wait&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}}]}
+test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+         &#39;client&#39;: &#39;Test GenericWait&#39;, &#39;plugin&#39;: &#39;GenericWait&#39;,
+         &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                    &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+         &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test GenericWait&#39;},
+                       &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                       &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                       &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}]}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test GenericWait&#39;,
+         &#39;command&#39;: &#39;wait&#39;, &#39;seconds&#39;: 0.02, &#39;id&#39;: &#39;Long Wait&#39;}
+test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test Wait&#39;, &#39;command&#39;: &#39;wait&#39;}
+test(): {&#39;sender&#39;: &#39;Test Wait&#39;, &#39;event&#39;: &#39;finished&#39;}
+test(): {&#39;sender&#39;: &#39;Test GenericWait&#39;, &#39;event&#39;: &#39;finished&#39;,
+         &#39;id&#39;: &#39;Long Wait&#39;}
+&#34;&#34;&#34;
+import asyncio
+
+from controlpi import BasePlugin, Message, MessageTemplate
+
+
+class Wait(BasePlugin):
+    &#34;&#34;&#34;Wait for time defined in configuration.
+
+    The &#34;seconds&#34; configuration key gets the number of seconds to wait after
+    receiving a &#34;wait&#34; command before sending the &#34;finished&#34; event:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Long Wait&#34;: {&#34;plugin&#34;: &#34;Wait&#34;, &#34;seconds&#34;: 0.02},
+    ...      &#34;Short Wait&#34;: {&#34;plugin&#34;: &#34;Wait&#34;, &#34;seconds&#34;: 0.01}},
+    ...     [{&#34;target&#34;: &#34;Long Wait&#34;, &#34;command&#34;: &#34;wait&#34;},
+    ...      {&#34;target&#34;: &#34;Short Wait&#34;, &#34;command&#34;: &#34;wait&#34;}], 0.025))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Long Wait&#39;, &#39;plugin&#39;: &#39;Wait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Long Wait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Short Wait&#39;, &#39;plugin&#39;: &#39;Wait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Short Wait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Long Wait&#39;, &#39;command&#39;: &#39;wait&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Short Wait&#39;, &#39;command&#39;: &#39;wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Short Wait&#39;, &#39;event&#39;: &#39;finished&#39;}
+    test(): {&#39;sender&#39;: &#39;Long Wait&#39;, &#39;event&#39;: &#39;finished&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;}},
+                   &#39;required&#39;: [&#39;seconds&#39;]}
+    &#34;&#34;&#34;Schema for Wait plugin configuration.
+
+    Required configuration key:
+
+    - &#39;seconds&#39;: number of seconds to wait.
+    &#34;&#34;&#34;
+
+    async def wait(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Wait configured time and send &#34;finished&#34; event.&#34;&#34;&#34;
+        async def wait_coroutine():
+            await asyncio.sleep(self.conf[&#39;seconds&#39;])
+            await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;}))
+        # Done in separate task to not block queue awaiting this callback:
+        asyncio.create_task(wait_coroutine())
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}})]
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}})]
+        self.bus.register(self.name, &#39;Wait&#39;, sends, receives, self.wait)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass
+
+
+class GenericWait(BasePlugin):
+    &#34;&#34;&#34;Wait for time defined in &#34;wait&#34; command.
+
+    The &#34;wait&#34; command has message keys &#34;seconds&#34; defining the seconds to
+    wait and &#34;id&#34; defining a string to be sent back in the &#34;finished&#34; event
+    after the wait:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test GenericWait&#34;: {&#34;plugin&#34;: &#34;GenericWait&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test GenericWait&#34;, &#34;command&#34;: &#34;wait&#34;,
+    ...       &#34;seconds&#34;: 0.02, &#34;id&#34;: &#34;Long Wait&#34;},
+    ...      {&#34;target&#34;: &#34;Test GenericWait&#34;, &#34;command&#34;: &#34;wait&#34;,
+    ...       &#34;seconds&#34;: 0.01, &#34;id&#34;: &#34;Short Wait&#34;}], 0.025))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test GenericWait&#39;, &#39;plugin&#39;: &#39;GenericWait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                        &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test GenericWait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                           &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                           &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test GenericWait&#39;,
+             &#39;command&#39;: &#39;wait&#39;, &#39;seconds&#39;: 0.02, &#39;id&#39;: &#39;Long Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test GenericWait&#39;,
+             &#39;command&#39;: &#39;wait&#39;, &#39;seconds&#39;: 0.01, &#39;id&#39;: &#39;Short Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Test GenericWait&#39;, &#39;event&#39;: &#39;finished&#39;,
+             &#39;id&#39;: &#39;Short Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Test GenericWait&#39;, &#39;event&#39;: &#39;finished&#39;,
+             &#39;id&#39;: &#39;Long Wait&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for GenericWait plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def wait(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Wait given time and send &#34;finished&#34; event with given &#34;id&#34;.&#34;&#34;&#34;
+        async def wait_coroutine():
+            assert isinstance(message[&#39;seconds&#39;], float)
+            await asyncio.sleep(message[&#39;seconds&#39;])
+            await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;,
+                                                    &#39;id&#39;: message[&#39;id&#39;]}))
+        # Done in separate task to not block queue awaiting this callback:
+        asyncio.create_task(wait_coroutine())
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                                     &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                     &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                                  &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+        self.bus.register(self.name, &#39;GenericWait&#39;, sends, receives, self.wait)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+</section>
+<section>
+<h2 class="section-title" id="header-classes">Classes</h2>
+<dl>
+<dt id="controlpi_plugins.wait.Wait"><code class="flex name class">
+<span>class <span class="ident">Wait</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Wait for time defined in configuration.</p>
+<p>The "seconds" configuration key gets the number of seconds to wait after
+receiving a "wait" command before sending the "finished" event:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Long Wait&quot;: {&quot;plugin&quot;: &quot;Wait&quot;, &quot;seconds&quot;: 0.02},
+...      &quot;Short Wait&quot;: {&quot;plugin&quot;: &quot;Wait&quot;, &quot;seconds&quot;: 0.01}},
+...     [{&quot;target&quot;: &quot;Long Wait&quot;, &quot;command&quot;: &quot;wait&quot;},
+...      {&quot;target&quot;: &quot;Short Wait&quot;, &quot;command&quot;: &quot;wait&quot;}], 0.025))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Long Wait', 'plugin': 'Wait',
+         'sends': [{'event': {'const': 'finished'}}],
+         'receives': [{'target': {'const': 'Long Wait'},
+                       'command': {'const': 'wait'}}]}
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Short Wait', 'plugin': 'Wait',
+         'sends': [{'event': {'const': 'finished'}}],
+         'receives': [{'target': {'const': 'Short Wait'},
+                       'command': {'const': 'wait'}}]}
+test(): {'sender': 'test()', 'target': 'Long Wait', 'command': 'wait'}
+test(): {'sender': 'test()', 'target': 'Short Wait', 'command': 'wait'}
+test(): {'sender': 'Short Wait', 'event': 'finished'}
+test(): {'sender': 'Long Wait', 'event': 'finished'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class Wait(BasePlugin):
+    &#34;&#34;&#34;Wait for time defined in configuration.
+
+    The &#34;seconds&#34; configuration key gets the number of seconds to wait after
+    receiving a &#34;wait&#34; command before sending the &#34;finished&#34; event:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Long Wait&#34;: {&#34;plugin&#34;: &#34;Wait&#34;, &#34;seconds&#34;: 0.02},
+    ...      &#34;Short Wait&#34;: {&#34;plugin&#34;: &#34;Wait&#34;, &#34;seconds&#34;: 0.01}},
+    ...     [{&#34;target&#34;: &#34;Long Wait&#34;, &#34;command&#34;: &#34;wait&#34;},
+    ...      {&#34;target&#34;: &#34;Short Wait&#34;, &#34;command&#34;: &#34;wait&#34;}], 0.025))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Long Wait&#39;, &#39;plugin&#39;: &#39;Wait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Long Wait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Short Wait&#39;, &#39;plugin&#39;: &#39;Wait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Short Wait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Long Wait&#39;, &#39;command&#39;: &#39;wait&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Short Wait&#39;, &#39;command&#39;: &#39;wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Short Wait&#39;, &#39;event&#39;: &#39;finished&#39;}
+    test(): {&#39;sender&#39;: &#39;Long Wait&#39;, &#39;event&#39;: &#39;finished&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = {&#39;properties&#39;: {&#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;}},
+                   &#39;required&#39;: [&#39;seconds&#39;]}
+    &#34;&#34;&#34;Schema for Wait plugin configuration.
+
+    Required configuration key:
+
+    - &#39;seconds&#39;: number of seconds to wait.
+    &#34;&#34;&#34;
+
+    async def wait(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Wait configured time and send &#34;finished&#34; event.&#34;&#34;&#34;
+        async def wait_coroutine():
+            await asyncio.sleep(self.conf[&#39;seconds&#39;])
+            await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;}))
+        # Done in separate task to not block queue awaiting this callback:
+        asyncio.create_task(wait_coroutine())
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}})]
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}})]
+        self.bus.register(self.name, &#39;Wait&#39;, sends, receives, self.wait)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.wait.Wait.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for Wait plugin configuration.</p>
+<p>Required configuration key:</p>
+<ul>
+<li>'seconds': number of seconds to wait.</li>
+</ul></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.wait.Wait.wait"><code class="name flex">
+<span>async def <span class="ident">wait</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Wait configured time and send "finished" event.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def wait(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Wait configured time and send &#34;finished&#34; event.&#34;&#34;&#34;
+    async def wait_coroutine():
+        await asyncio.sleep(self.conf[&#39;seconds&#39;])
+        await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;}))
+    # Done in separate task to not block queue awaiting this callback:
+    asyncio.create_task(wait_coroutine())</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.wait.Wait.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;}})]
+    sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;}})]
+    self.bus.register(self.name, &#39;Wait&#39;, sends, receives, self.wait)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.wait.Wait.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+<dt id="controlpi_plugins.wait.GenericWait"><code class="flex name class">
+<span>class <span class="ident">GenericWait</span></span>
+<span>(</span><span>bus: <a title="controlpi.messagebus.MessageBus" href="../controlpi/messagebus.html#controlpi.messagebus.MessageBus">MessageBus</a>, name: str, conf: Dict[str, Any])</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Wait for time defined in "wait" command.</p>
+<p>The "wait" command has message keys "seconds" defining the seconds to
+wait and "id" defining a string to be sent back in the "finished" event
+after the wait:</p>
+<pre><code class="language-python-repl">&gt;&gt;&gt; import controlpi
+&gt;&gt;&gt; asyncio.run(controlpi.test(
+...     {&quot;Test GenericWait&quot;: {&quot;plugin&quot;: &quot;GenericWait&quot;}},
+...     [{&quot;target&quot;: &quot;Test GenericWait&quot;, &quot;command&quot;: &quot;wait&quot;,
+...       &quot;seconds&quot;: 0.02, &quot;id&quot;: &quot;Long Wait&quot;},
+...      {&quot;target&quot;: &quot;Test GenericWait&quot;, &quot;command&quot;: &quot;wait&quot;,
+...       &quot;seconds&quot;: 0.01, &quot;id&quot;: &quot;Short Wait&quot;}], 0.025))
+... # doctest: +NORMALIZE_WHITESPACE
+test(): {'sender': '', 'event': 'registered',
+         'client': 'Test GenericWait', 'plugin': 'GenericWait',
+         'sends': [{'event': {'const': 'finished'},
+                    'id': {'type': 'string'}}],
+         'receives': [{'target': {'const': 'Test GenericWait'},
+                       'command': {'const': 'wait'},
+                       'seconds': {'type': 'number'},
+                       'id': {'type': 'string'}}]}
+test(): {'sender': 'test()', 'target': 'Test GenericWait',
+         'command': 'wait', 'seconds': 0.02, 'id': 'Long Wait'}
+test(): {'sender': 'test()', 'target': 'Test GenericWait',
+         'command': 'wait', 'seconds': 0.01, 'id': 'Short Wait'}
+test(): {'sender': 'Test GenericWait', 'event': 'finished',
+         'id': 'Short Wait'}
+test(): {'sender': 'Test GenericWait', 'event': 'finished',
+         'id': 'Long Wait'}
+</code></pre></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">class GenericWait(BasePlugin):
+    &#34;&#34;&#34;Wait for time defined in &#34;wait&#34; command.
+
+    The &#34;wait&#34; command has message keys &#34;seconds&#34; defining the seconds to
+    wait and &#34;id&#34; defining a string to be sent back in the &#34;finished&#34; event
+    after the wait:
+    &gt;&gt;&gt; import controlpi
+    &gt;&gt;&gt; asyncio.run(controlpi.test(
+    ...     {&#34;Test GenericWait&#34;: {&#34;plugin&#34;: &#34;GenericWait&#34;}},
+    ...     [{&#34;target&#34;: &#34;Test GenericWait&#34;, &#34;command&#34;: &#34;wait&#34;,
+    ...       &#34;seconds&#34;: 0.02, &#34;id&#34;: &#34;Long Wait&#34;},
+    ...      {&#34;target&#34;: &#34;Test GenericWait&#34;, &#34;command&#34;: &#34;wait&#34;,
+    ...       &#34;seconds&#34;: 0.01, &#34;id&#34;: &#34;Short Wait&#34;}], 0.025))
+    ... # doctest: +NORMALIZE_WHITESPACE
+    test(): {&#39;sender&#39;: &#39;&#39;, &#39;event&#39;: &#39;registered&#39;,
+             &#39;client&#39;: &#39;Test GenericWait&#39;, &#39;plugin&#39;: &#39;GenericWait&#39;,
+             &#39;sends&#39;: [{&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                        &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}],
+             &#39;receives&#39;: [{&#39;target&#39;: {&#39;const&#39;: &#39;Test GenericWait&#39;},
+                           &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                           &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                           &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}}]}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test GenericWait&#39;,
+             &#39;command&#39;: &#39;wait&#39;, &#39;seconds&#39;: 0.02, &#39;id&#39;: &#39;Long Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;test()&#39;, &#39;target&#39;: &#39;Test GenericWait&#39;,
+             &#39;command&#39;: &#39;wait&#39;, &#39;seconds&#39;: 0.01, &#39;id&#39;: &#39;Short Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Test GenericWait&#39;, &#39;event&#39;: &#39;finished&#39;,
+             &#39;id&#39;: &#39;Short Wait&#39;}
+    test(): {&#39;sender&#39;: &#39;Test GenericWait&#39;, &#39;event&#39;: &#39;finished&#39;,
+             &#39;id&#39;: &#39;Long Wait&#39;}
+    &#34;&#34;&#34;
+
+    CONF_SCHEMA = True
+    &#34;&#34;&#34;Schema for GenericWait plugin configuration.
+
+    There are no required or optional configuration keys.
+    &#34;&#34;&#34;
+
+    async def wait(self, message: Message) -&gt; None:
+        &#34;&#34;&#34;Wait given time and send &#34;finished&#34; event with given &#34;id&#34;.&#34;&#34;&#34;
+        async def wait_coroutine():
+            assert isinstance(message[&#39;seconds&#39;], float)
+            await asyncio.sleep(message[&#39;seconds&#39;])
+            await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;,
+                                                    &#39;id&#39;: message[&#39;id&#39;]}))
+        # Done in separate task to not block queue awaiting this callback:
+        asyncio.create_task(wait_coroutine())
+
+    def process_conf(self) -&gt; None:
+        &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+        receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                     &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                                     &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                     &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+        sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                                  &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+        self.bus.register(self.name, &#39;GenericWait&#39;, sends, receives, self.wait)
+
+    async def run(self) -&gt; None:
+        &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+        pass</code></pre>
+</details>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li><a title="controlpi.baseplugin.BasePlugin" href="../controlpi/baseplugin.html#controlpi.baseplugin.BasePlugin">BasePlugin</a></li>
+<li>abc.ABC</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="controlpi_plugins.wait.GenericWait.CONF_SCHEMA"><code class="name">var <span class="ident">CONF_SCHEMA</span></code></dt>
+<dd>
+<div class="desc"><p>Schema for GenericWait plugin configuration.</p>
+<p>There are no required or optional configuration keys.</p></div>
+</dd>
+</dl>
+<h3>Methods</h3>
+<dl>
+<dt id="controlpi_plugins.wait.GenericWait.wait"><code class="name flex">
+<span>async def <span class="ident">wait</span></span>(<span>self, message: <a title="controlpi.messagebus.Message" href="../controlpi/messagebus.html#controlpi.messagebus.Message">Message</a>) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Wait given time and send "finished" event with given "id".</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def wait(self, message: Message) -&gt; None:
+    &#34;&#34;&#34;Wait given time and send &#34;finished&#34; event with given &#34;id&#34;.&#34;&#34;&#34;
+    async def wait_coroutine():
+        assert isinstance(message[&#39;seconds&#39;], float)
+        await asyncio.sleep(message[&#39;seconds&#39;])
+        await self.bus.send(Message(self.name, {&#39;event&#39;: &#39;finished&#39;,
+                                                &#39;id&#39;: message[&#39;id&#39;]}))
+    # Done in separate task to not block queue awaiting this callback:
+    asyncio.create_task(wait_coroutine())</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.wait.GenericWait.process_conf"><code class="name flex">
+<span>def <span class="ident">process_conf</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Register plugin as bus client.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">def process_conf(self) -&gt; None:
+    &#34;&#34;&#34;Register plugin as bus client.&#34;&#34;&#34;
+    receives = [MessageTemplate({&#39;target&#39;: {&#39;const&#39;: self.name},
+                                 &#39;command&#39;: {&#39;const&#39;: &#39;wait&#39;},
+                                 &#39;seconds&#39;: {&#39;type&#39;: &#39;number&#39;},
+                                 &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+    sends = [MessageTemplate({&#39;event&#39;: {&#39;const&#39;: &#39;finished&#39;},
+                              &#39;id&#39;: {&#39;type&#39;: &#39;string&#39;}})]
+    self.bus.register(self.name, &#39;GenericWait&#39;, sends, receives, self.wait)</code></pre>
+</details>
+</dd>
+<dt id="controlpi_plugins.wait.GenericWait.run"><code class="name flex">
+<span>async def <span class="ident">run</span></span>(<span>self) ‑> NoneType</span>
+</code></dt>
+<dd>
+<div class="desc"><p>Run no code proactively.</p></div>
+<details class="source">
+<summary>
+<span>Expand source code</span>
+</summary>
+<pre><code class="python">async def run(self) -&gt; None:
+    &#34;&#34;&#34;Run no code proactively.&#34;&#34;&#34;
+    pass</code></pre>
+</details>
+</dd>
+</dl>
+</dd>
+</dl>
+</section>
+</article>
+<nav id="sidebar">
+<h1>Index</h1>
+<div class="toc">
+<ul></ul>
+</div>
+<ul id="index">
+<li><h3>Super-module</h3>
+<ul>
+<li><code><a title="controlpi_plugins" href="index.html">controlpi_plugins</a></code></li>
+</ul>
+</li>
+<li><h3><a href="#header-classes">Classes</a></h3>
+<ul>
+<li>
+<h4><code><a title="controlpi_plugins.wait.Wait" href="#controlpi_plugins.wait.Wait">Wait</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.wait.Wait.wait" href="#controlpi_plugins.wait.Wait.wait">wait</a></code></li>
+<li><code><a title="controlpi_plugins.wait.Wait.process_conf" href="#controlpi_plugins.wait.Wait.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.wait.Wait.run" href="#controlpi_plugins.wait.Wait.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.wait.Wait.CONF_SCHEMA" href="#controlpi_plugins.wait.Wait.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+<li>
+<h4><code><a title="controlpi_plugins.wait.GenericWait" href="#controlpi_plugins.wait.GenericWait">GenericWait</a></code></h4>
+<ul class="">
+<li><code><a title="controlpi_plugins.wait.GenericWait.wait" href="#controlpi_plugins.wait.GenericWait.wait">wait</a></code></li>
+<li><code><a title="controlpi_plugins.wait.GenericWait.process_conf" href="#controlpi_plugins.wait.GenericWait.process_conf">process_conf</a></code></li>
+<li><code><a title="controlpi_plugins.wait.GenericWait.run" href="#controlpi_plugins.wait.GenericWait.run">run</a></code></li>
+<li><code><a title="controlpi_plugins.wait.GenericWait.CONF_SCHEMA" href="#controlpi_plugins.wait.GenericWait.CONF_SCHEMA">CONF_SCHEMA</a></code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</nav>
+</main>
+<footer id="footer">
+<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p>
+</footer>
+</body>
+</html>
\ No newline at end of file