werden soll) in das Verzeichnis für das neue Plugin verschoben oder kopiert
werden.
In diesem Verzeichnis löschen wir zunächst das `.git`-Verzeichnis (`rm -rf
-.git/`), da wir Änderungen nicht das Template-Repository, sondern (falls
+.git/`), da wir Änderungen nicht in das Template-Repository, sondern (falls
überhaupt) in ein neues Repository für das neue Plugin einchecken wollen.
Für unser Beispiel führen wir also
```shell
-$ cp -R controlpi-template controlpi-overshoot
+$ cp -r controlpi-template controlpi-overshoot
$ cd controlpi-overshoot
$ rm -rf .git/
```
In `setup.py` können auch Abhängigkeiten zu anderen Python-Bibliotheken
deklariert werden.
+`README.md`:
+```markdown
+# ControlPi Plugin for Giving a State Overshoot Time
+This distribution package contains a plugin for the
+[ControlPi system](https://docs.graph-it.com/graphit/controlpi), that
+lets a state active for a configured time after setting its controlling
+overshoot state to `False`.
+
+Documentation (in German) can be found at [doc/index.md](doc/index.md) in
+the source repository and at
+[https://docs.graph-it.com/graphit/controlpi-overshoot/](https://docs.graph-it.com/graphit/controlpi-overshoot/).
+Code documentation (in English) including doctests is contained in the
+source files.
+```
+
+`setup.py`:
+```python
+import setuptools
+
+with open("README.md", "r") as readme_file:
+ long_description = readme_file.read()
+
+setuptools.setup(
+ name="controlpi-overshoot",
+ version="0.1.0",
+ author="Graph-IT GmbH",
+ author_email="info@graph-it.com",
+ description="ControlPi Plugin for Giving a State Overshoot Time",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="http://docs.graph-it.com/graphit/controlpi-overshoot",
+ packages=["controlpi_plugins"],
+ install_requires=[
+ "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git",
+ ],
+ classifiers=[
+ "Programming Language :: Python",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ ],
+)
+```
+
In `doc/index.md` befindet sich die längere Dokumentation unseres neuen
Plugins (auf Deutsch).
In `conf.json` befindet sich eine minimale Beispiel-Konfiguration für
haben und diese noch einen Platzhalter enthält, passiert hier noch nichts.
## Implementierung des Plugins selbst
+Das Plugin wird wie gesagt in `controlpi_plugins/overshoot.py`
+implementiert:
+```python
+"""ControlPi Plugin for Giving a State Overshoot Time."""
+import asyncio
+
+from controlpi import BasePlugin, Message, MessageTemplate
+from controlpi.baseplugin import JSONSchema
+```
+Zunächst werden die notwendigen Module und einzelne Klassen aus diesen
+importiert.
+Für unser einfaches Beispiel brauchen wir nur das Modul `asyncio` für
+asynchrone Programmierung und die Klassen `BasePlugin`, `Message` und
+`MessageTemplate` aus dem ControlPi-Basis-System.
+`JSONSchema` ist eine Typ-Definition für JSON-Schemata.
+```python
+class Overshoot(BasePlugin):
+ """ControlPi plugin for Giving a State Overshoot Time."""
+```
+
+Jedes ControlPi-Plugin erbt von der Klasse `BasePlugin`, die unter anderem
+festlegt, dass die weiter unten besprochenen Methoden `process_conf` und
+`run` implementiert werden müssen.
### Konfigurations-Schema
+Die Konfiguration eines Plugins wird durch die Konstante `CONF_SCHEMA` als
+JSON-Schema beschrieben:
+```python
+ CONF_SCHEMA: JSONSchema = {'properties':
+ {'state': {'type': 'string'},
+ 'overshoot': {'type': 'integer',
+ 'default': 10}},
+ 'required': ['state']}
+```
+
+Für unser Beispiel gibt es zwei Attribute in der Konfiguration. `state` ist
+ein String, der festlegt, welcher andere Zustand durch den Nachlauf
+geschaltet werden soll, und `overshoot` ist ein Integer, der festlegt, wie
+lang der Nachlauf in Sekunden sein soll.
+Für `overshoot` ist ein Default-Wert von 10 Sekunden vorgegeben, während
+für `state` festgelegt ist, dass dieses Attribut in der Konfiguration
+vorhanden sein muss.
+
+Die Methode `process_conf` wird für alle Plugins während der
+Initialisierung des ControlPi-Systems synchron aufgerufen.
+Sie hat die Aufgabe, die Konfiguration abzuarbeiten und eventuelle
+Instanz-Attribute zu setzen:
+```python
+ def process_conf(self) -> None:
+ """Register bus client."""
+ self._state = False
+ self._delayed_off = 0
+ self._cancelled_off = 0
+```
+
+In unserem Beispiel muss von der Konfiguration nichts bearbeitet werden.
+Wir brauchen aber einige Instanz-Attribute:
+`self._state` hält den momentanen Zustand, `self._delayed_off` die Anzahl
+der Ausschalt-Vorgänge, die noch auf ihre Abarbeitung warten und
+`self._cancelled_off` die Anzahl der durch Wieder-Einschalten abgebrochenen
+Ausschalt-Vorgänge.
### Registrierung am Message-Bus
+Außerdem wird in `process_conf` auch häufig das Plugin am Message-Bus
+registriert (wenn es nicht – wie beispielsweise ein Web-Server – mehrere
+Klienten nach Bedarf registriert):
+```python
+ self.bus.register(self.name, 'Overshoot',
+ [MessageTemplate({'event':
+ {'const': 'changed'},
+ 'state':
+ {'type': 'boolean'}}),
+ MessageTemplate({'state':
+ {'type': 'boolean'}}),
+ MessageTemplate({'target':
+ {'const': self.conf['state']},
+ 'command':
+ {'const': 'set state'},
+ 'new state':
+ {'type': 'boolean'}})],
+ [([MessageTemplate({'target':
+ {'const': self.name},
+ 'command':
+ {'const': 'get state'}})],
+ self._get_state),
+ ([MessageTemplate({'target':
+ {'const': self.name},
+ 'command':
+ {'const': 'set state'},
+ 'new state':
+ {'type': 'boolean'}})],
+ self._set_state)])
+```
+
+Die Registrierung enthält zunächst den Namen der Instanz und des Plugins.
+Dann folgt ein Array mit Nachrichten-Templates für alle Nachrichten, die
+das Plugin senden möchte.
+In unserem Fall sind dies `'state'`-Nachrichten, die den momentanen Zustand
+mitteilen, optional mit einem Attribut `'event': 'changed'`, das angibt,
+dass sich der Zustand geändert hat, und `'command'`-Nachrichten an den
+untergeordneten, von unserem Nachlauf-Plugin kontrollierten Zustand.
+
+Nach den gesendeten Nachrichten werden die empfangenen Nachrichten
+konfiguriert, hierbei wird für ein Array von Nachrichten-Templates jeweils
+ein Callback angegeben, das für diese Nachrichten aufgerufen werden soll.
+Für `'command': 'get state'`-Nachrichten ist dies in unserem Fall die
+Methode `self._get_state` und für `'command': 'set state'`-Nachrichten die
+Methode `self._set_state`, wobei im Template zusätzlich spezifiziert ist,
+dass `'command': 'set state'`-Nachrichten ein `'boolean'`-Attribut
+`'new state'` enthalten müssen.
### Callbacks
+Ein großer Teil der Funktionalität von Plugins wird durch solche Callbacks
+implementiert, die auf Nachrichten von anderen Clients im ControlPi-System
+reagieren.
+
+In unserem Fall sind dies explizite Kommandos, den momentanen Zustand
+auszulesen oder zu ändern, aber es kann auch auf beliebige andere
+Nachrichten – beispielsweise die von unserem Beispiel-Plugin gesendeten
+`'event'`- und `'state'`-Nachrichten – reagiert werden.
+
+Das Callback `self._get_state` sendet einfach den momentanen Zustand auf
+den Bus:
+```python
+ async def _get_state(self, message) -> None:
+ await self.bus.send(Message(self.name, {'state': self._state}))
+```
+
+Das Callback `self._set_state` sendet eine `'event': 'changed'`-Nachricht,
+wenn der Zustand sich geändert hat:
+```python
+ async def _set_state(self, message) -> None:
+ if self._state != message['new state']:
+ self._state = message['new state']
+ await self.bus.send(Message(self.name,
+ {'event': 'changed',
+ 'state': self._state}))
+```
+
+Hat er sich zu `True` geändert, werden alle Ausschalt-Vorgänge abgebrochen,
+indem die Zahl der abgebrochenen auf die Zahl der noch wartenden
+Ausschalt-Vorgänge gesetzt wird.
+Außerdem wird durch eine `'command'`-Nachricht sichergestellt, dass der
+kontrollierte Zustand ebenfalls `True` ist:
+```python
+ if message['new state']:
+ # Cancel all turning off:
+ self._cancelled_off = self._delayed_off
+ await self.bus.send(Message(self.name,
+ {'target': self.conf['state'],
+ 'command': 'set state',
+ 'new state': True}))
+```
+
+Hat er sich zu `False` geändert, wird die Anzahl der Ausschalt-Vorgänge um
+1 erhöht und die konfigurierte Zeit gewartet.
+Wenn nach diesem Warten die Anzahl der abgebrochenen Vorgänge noch größer
+als 0 ist, wird diese um 1 reduziert und nichts weiter unternommen.
+Nur, wenn die abgebrochenen Vorgänge schon 0 waren, wird der kontrollierte
+Zustand auf `False` gesetzt:
+```python
+ else:
+ self._delayed_off += 1
+ await asyncio.sleep(self.conf['overshoot'])
+ if self._cancelled_off > 0:
+ self._cancelled_off -= 1
+ else:
+ await self.bus.send(Message(self.name,
+ {'target': self.conf['state'],
+ 'command': 'set state',
+ 'new state': False}))
+ self._delayed_off -= 1
+```
+
+Wenn der Zustand sich durch `'command': 'set state'` gar nicht geändert
+hat, wird einfach nur der aktuelle Zustand als Nachricht gemeldet:
+```python
+ else:
+ await self.bus.send(Message(self.name,
+ {'state': self._state}))
+```
+
+Schließlich muss jede Implementierung von `BasePlugin` eine asynchrone
+`run`-Methode enthalten, die durch das ControlPi-System für alle Plugins
+gestartet wird:
+```python
+ async def run(self) -> None:
+ """Do nothing."""
+ pass
+```
+
+Für unser Beispiel-Plugin hat diese nichts zu tun.
+Für andere Plugins kann sie vor allem von der Kommunikation mit anderen
+Clients unabhängige Kommunikation mit der Außenwelt implementieren:
+* Starten eines Web-Servers, der auf eingehende Verbindungen wartet
+* Starten eines parallelen Threads, der Peripherie-Ereignisse in
+Bus-Nachrichten übersetzt
+* Starten eines parallelen Threads, der eine Schnittstelle überwacht
+
+### Beispiel-Konfiguration
+Die Beispiel-Konfiguration sieht so aus:
+```json
+{
+ "Debug": {
+ "plugin": "WSServer",
+ "port": 8000,
+ "web": {
+ "/": {
+ "module": "controlpi_plugins.wsserver",
+ "location": "Debug"
+ }
+ }
+ },
+ "Light": {
+ "plugin": "State"
+ },
+ "Light-Overshoot": {
+ "plugin": "Overshoot",
+ "state": "Light"
+ }
+}
+```
+
+Es bietet sich oft an, das Beispiel mit einer Debug-Oberfläche
+auszustatten.
+Um es laufen zu lassen, muss dann `controlpi-wsserver` zusätzlich (bei
+aktiviertem Venv) installiert werden:
+```shell
+$ pip install git+git://git.graph-it.com/graphit/controlpi-wsserver.git
+```
+
+Außer dem Websocket-Server, der Debug-Oberfläche enthält dieses Beispiel
+nur ein `State`-Plugin `Light`, das den kontrollierten Zustand – ein Licht,
+das nach dem Ausschalten eine Weile weiterleuchten soll – darstellt, und
+ein `Overshoot`-Plugin `Light-Overshoot`, das eben ein Beispiel für das
+hier implementierte Plugin ist.
+
+Es ist in diesen Minimal-Beispielen oft sinnvoll Dinge, die in der
+tatsächlichen Anwendung wahrscheinlich geschaltete elektrische Ausgänge –
+z.B. durch das Plugin `controlpi-pinio` – wären durch einfache
+`State`-Plugins darzustellen, da diese ohne externe Abhängigkeiten
+funktionieren und in der Debug-Oberfläche betrachtet werden können.
+
+Das Beispiel kann (immer noch bei aktivierten Venv) ausgeführt werden
+durch:
+```shell
+$ python -m controlpi conf.json
+```
+Ein Zustand dieses Beispiels sieht zum Beispiel so aus:
+
+
+Das `Overshoot`-Plugin wurde bereits ausgeschaltet, das `State`-Plugin ist
+aber noch eingeschaltet und wird genau 10 Sekunden (da der Default in der
+Beispiel-Konfiguration nicht überschrieben wurde) später ausgehen.