Adapt to changes in controlpi/controlpi-wsserver.
authorBenjamin Braatz <bb@bbraatz.eu>
Tue, 23 Mar 2021 14:12:40 +0000 (15:12 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Tue, 23 Mar 2021 14:16:13 +0000 (15:16 +0100)
conf.json
controlpi-plugins/statemachine.py [deleted file]
controlpi_plugins/statemachine.py [new file with mode: 0644]
setup.py

index d94fb6f6b094d2fca021f632815f3a70f7273139..7429043d6815c80d51c91ea221c4c41e89f5e58d 100644 (file)
--- a/conf.json
+++ b/conf.json
@@ -2,7 +2,10 @@
     "Example Server": {
         "plugin": "WSServer",
         "port": 8080,
-        "web root": "../controlpi-wsserver/web"
+        "web": {
+            "/": { "module": "controlpi_plugins.wsserver",
+                   "location": "Debug" }
+        }
     },
     "Lubrication Button": {
         "plugin": "Alias",
                 ]
             }
         }
-    },
-    "Debug Logger": {
-        "plugin": "Log",
-        "filter": [
-            {}
-        ]
     }
 }
diff --git a/controlpi-plugins/statemachine.py b/controlpi-plugins/statemachine.py
deleted file mode 100644 (file)
index 1732398..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-"""Provide …
-
-TODO: documentation, doctests
-"""
-import jsonschema  # type: ignore
-from typing import Mapping, Any
-
-from controlpi import BasePlugin, Message, PluginConfiguration
-
-
-def template_from_message(message: Message) -> Message:
-    template = {}
-    for key in message:
-        value = message[key]
-        if (isinstance(value, bool) or isinstance(value, int) or
-                isinstance(value, float) or isinstance(value, str)):
-            value = {'const': value}
-        elif (isinstance(value, dict)):
-            value = {'type': 'object',
-                     'properties': template_from_message(value)}
-        template[key] = value
-    return template
-
-
-class StateMachine(BasePlugin):
-    async def _receive(self, message: Message) -> None:
-        if ('target' in message and message['target'] == self._name and
-                'command' in message):
-            if message['command'] == 'get state':
-                answer = {'sender': self._name,
-                          'state': self._current_state}
-                await self._bus.send(answer)
-            if message['command'] == 'get machine':
-                answer = {'sender': self._name,
-                          'init': self._init,
-                          'states': self._states}
-                await self._bus.send(answer)
-        for transition in self._states[self._current_state]['transitions']:
-            for trigger in transition['triggers']:
-                matches = True
-                for key in trigger:
-                    if key not in message:
-                        matches = False
-                        break
-                    try:
-                        jsonschema.validate(message[key], trigger[key])
-                    except jsonschema.exceptions.ValidationError:
-                        matches = False
-                        break
-                if matches:
-                    new_state = transition['to']
-                    self._current_state: str = new_state
-                    for message in self._states[new_state]['commands']:
-                        complete_message = {'sender': self._name}
-                        complete_message.update(message)
-                        await self._bus.send(complete_message)
-                    event = {'sender': self._name, 'event': 'changed',
-                             'state': new_state}
-                    await self._bus.send(event)
-                    break
-
-    def _process_conf(self, conf: PluginConfiguration) -> None:
-        self._init = conf['init']
-        self._current_state = ''
-        self._states: Mapping[str, Any] = conf['states']
-        sends: list[Message] = [{'event': {'const': 'changed'},
-                                 'state': {'type': 'string'}},
-                                {'state': {'type': 'string'}},
-                                {'init': {'type': 'string'}, 'states':
-                                 {'type': 'object', 'patternProperties':
-                                  {'.+': {'type': 'object', 'properties':
-                                   {'commands': {'type': 'array'},
-                                    'transitions': {'type': 'array'}},
-                                   'additionalProperties': False}},
-                                  'additionalProperties': False}}]
-        receives: list[Message] = [{'target': {'const': self._name},
-                                    'command': {'const': 'get state'}},
-                                   {'target': {'const': self._name},
-                                    'command': {'const': 'get machine'}}]
-        for state in self._states:
-            for message in self._states[state]['commands']:
-                sends.append(template_from_message(message))
-            for transition in self._states[state]['transitions']:
-                for template in transition['triggers']:
-                    receives.append(template)
-        self._bus.register(self._name, 'StateMachine',
-                           sends, receives, self._receive)
-        super()._process_conf(conf)
-
-    async def run(self) -> None:
-        await super().run()
-        self._current_state = self._init
-        for message in self._states[self._init]['commands']:
-            complete_message = {'sender': self._name}
-            complete_message.update(message)
-            await self._bus.send(complete_message)
diff --git a/controlpi_plugins/statemachine.py b/controlpi_plugins/statemachine.py
new file mode 100644 (file)
index 0000000..a68e04f
--- /dev/null
@@ -0,0 +1,115 @@
+"""Provide state machine plugin.
+
+…
+
+TODO: documentation, doctests
+"""
+from controlpi import BasePlugin, Message, MessageTemplate
+
+
+class StateMachine(BasePlugin):
+    """State machine plugin.
+
+    Reacts to messages matching message templates at transitions by
+    switching states and sending messages defined for these states.
+    """
+
+    CONF_SCHEMA = {'properties':
+                   {'init': {'type': 'string'},
+                    'states':
+                    {'type': 'object',
+                     'patternProperties':
+                     {'^[A-Za-z ]+$':
+                      {'type': 'object',
+                       'properties':
+                       {'commands':
+                        {'type': 'array', 'items': {'type': 'object'}},
+                        'transitions':
+                        {'type': 'array',
+                         'items':
+                         {'type': 'object',
+                          'properties':
+                          {'triggers':
+                           {'type': 'array', 'items': {'type': 'object'}},
+                           'to': {'type': 'string'}},
+                          'required': ['triggers', 'to']}}},
+                       'required': ['commands', 'transitions']}},
+                     'additionalProperties': False}},
+                   'required': ['init', 'states']}
+
+    async def _receive(self, message: Message) -> None:
+        if ('target' in message and message['target'] == self.name and
+                'command' in message):
+            if message['command'] == 'get state':
+                await self.bus.send(Message(self.name,
+                                            {'state': self._current_state}))
+            if message['command'] == 'get machine':
+                await self.bus.send(Message(self.name,
+                                            {'init': self.conf['init'],
+                                             'states': self.conf['states']}))
+        transitions = self.conf['states'][self._current_state]['transitions']
+        for transition in transitions:
+            for trigger in transition['triggers']:
+                if MessageTemplate(trigger).check(message):
+                    new_state = transition['to']
+                    self._current_state: str = new_state
+                    await self.bus.send(Message(self.name,
+                                                {'event': 'changed',
+                                                 'state': new_state}))
+                    commands = self.conf['states'][new_state]['commands']
+                    for command in commands:
+                        await self.bus.send(Message(self.name, command))
+                    break
+
+    def process_conf(self) -> None:
+        """Register plugin as bus client."""
+        self._current_state = ''
+        sends = []
+        sends.append(MessageTemplate({'event': {'const': 'changed'},
+                                      'state': {'type': 'string'}}))
+        sends.append(MessageTemplate({'state': {'type': 'string'}}))
+        sends.append(MessageTemplate({'init': {'type': 'string'},
+                                      'states':
+                                      {'type': 'object',
+                                       'patternProperties':
+                                       {'^[A-Za-z ]+$':
+                                        {'type': 'object',
+                                         'properties':
+                                         {'commands':
+                                          {'type': 'array',
+                                           'items': {'type': 'object'}},
+                                          'transitions':
+                                          {'type': 'array',
+                                           'items':
+                                           {'type': 'object',
+                                            'properties':
+                                            {'triggers':
+                                             {'type': 'array',
+                                              'items': {'type': 'object'}},
+                                             'to': {'type': 'string'}},
+                                            'required': ['triggers', 'to']}}},
+                                         'required': ['commands',
+                                                      'transitions']}},
+                                       'additionalProperties': False}}))
+        receives = []
+        receives.append(MessageTemplate({'target': {'const': self.name},
+                                         'command': {'const': 'get state'}}))
+        receives.append(MessageTemplate({'target': {'const': self.name},
+                                         'command': {'const': 'get machine'}}))
+        for state in self.conf['states']:
+            commands = self.conf['states'][state]['commands']
+            for command in commands:
+                sends.append(MessageTemplate.from_message(command))
+            transitions = self.conf['states'][state]['transitions']
+            for transition in transitions:
+                for template in transition['triggers']:
+                    receives.append(MessageTemplate(template))
+        self.bus.register(self.name, 'StateMachine',
+                          sends, receives, self._receive)
+
+    async def run(self) -> None:
+        """Go into initial state."""
+        self._current_state = self.conf['init']
+        commands = self.conf['states'][self.conf['init']]['commands']
+        for command in commands:
+            await self.bus.send(Message(self.name, command))
index e43100d5a543fbf6ae3d7f9320cf368170fc01c8..c2997509e7f9a4a6e16e6aeca38e2a187c1ba076 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ setuptools.setup(
     long_description=long_description,
     long_description_content_type="text/markdown",
     url="http://docs.graph-it.com/graphit/controlpi-statemachine",
-    packages=["controlpi-plugins"],
+    packages=["controlpi_plugins"],
     install_requires=[                                                                              
         "jsonschema",
         "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git",