"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": [
- {}
- ]
}
}
+++ /dev/null
-"""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)
--- /dev/null
+"""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))
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",