diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..f1e6035ec7a99a07a4516726f7fca4c83ec0ba14
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,2 @@
+FROM nexus.engageska-portugal.pt/ska-tango-images/pytango-builder:9.3.3.3 AS buildenv
+RUN apt-get update && apt-get install gnupg2 -y
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000000000000000000000000000000000..dfe337ba06c14bda2b83c1e9819551d0a6d79d69
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,30 @@
+{
+    "name": "ska_tango_base vscode development container",
+    "context": "..",
+    "dockerFile": "Dockerfile",
+    "settings": {
+        "terminal.integrated.shell.linux": "/bin/bash",
+        "python.pythonPath": "/usr/bin/python3",
+        "files.exclude": {
+            ".coverage*": true,
+            "**/*.egg*": true,
+            "**/__pycache__": true
+        },
+        "files.watcherExclude": {
+            ".coverage*": true,
+            "**/*.egg*": true,
+            "**/__pycache__": true
+        }
+    },
+    "extensions": [
+        "ms-python.python",
+        "lextudio.restructuredtext",
+        "davidanson.vscode-markdownlint",
+        "alexkrechik.cucumberautocomplete",
+    ],
+    "postCreateCommand": "python3 -m pip install -e /workspaces/ska-tango-base --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple",
+    "remoteUser": "tango",
+    "remoteEnv": {
+        "PATH": "/home/tango/.local/bin:${containerEnv:PATH}",
+    }
+}
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..ed38118cedb2c12731c55a6f0710c7a8faec5066
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+.devcontainer/
+.pytest_cache/
+build/
+htmlcov/
+**/__pycache__/
diff --git a/.gitignore b/.gitignore
index 5ac491b20015b04696ab123bcc180fd95c862bd1..49b350ed0986094e1b81f3cf3f8d93a1d94b7018 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,7 @@ __pycache__
 pip-log.txt
 
 # Unit test / coverage reports
-.coverage
+.coverage*
 .tox
 .cache
 nosetests.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e972b03473337459b9c133315e4fd7fed2973b33..3594565cb10e21e60ee06db8205bc2763f1c7d59 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,7 @@
 # GitLab CI in conjunction with GitLab Runner can use Docker Engine to test and build any application.
 # Docker, when used with GitLab CI, runs each job in a separate and isolated container using the predefined image that is set up in .gitlab-ci.yml.
 # In this case we use the latest python docker image to build and test this project.
-image: nexus.engageska-portugal.pt/ska-docker/ska-python-buildenv:9.3.3.1
+image: nexus.engageska-portugal.pt/ska-tango-images/pytango-builder:9.3.3.3
 
 include:
   - project: 'ska-telescope/templates-repository'
@@ -64,7 +64,7 @@ unit tests:
     - k8srunner
   script:
     - echo $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
-    - python3 -m pip install -U $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
+    - python3 -m pip install --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple -U $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
     - make test
     - scripts/validate-metadata.sh
   artifacts:
@@ -77,7 +77,7 @@ linting:
     - k8srunner
   script:
     - echo $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
-    - python3 -m pip install -U $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
+    - python3 -m pip install --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple -U $(ls -d ./dist/*.whl | grep $CI_COMMIT_SHORT_SHA)
     - make lint
   artifacts:
     paths:
diff --git a/.release b/.release
index d0f247ddee30ef3cb1266b9f7e23643c622d7902..b8cad3a06ad5dd71d35dacb6434a2951deba451a 100644
--- a/.release
+++ b/.release
@@ -1,2 +1,2 @@
-release=0.10.1
-tag=ska_tango_base-0.10.1
+release=0.11.0
+tag=ska_tango_base-0.11.0
diff --git a/Dockerfile b/Dockerfile
index 0195ae70305b769f0bb00ec515eff681f144c0a5..00b73e381dd2c9a6aee2f28912e9edb0574fe218 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 # Use SKA python image as base image
-FROM nexus.engageska-portugal.pt/ska-docker/ska-python-buildenv:9.3.3.1 AS buildenv
-FROM nexus.engageska-portugal.pt/ska-docker/ska-python-runtime:9.3.3.1 AS runtime
+FROM nexus.engageska-portugal.pt/ska-tango-images/pytango-builder:9.3.3.3 AS buildenv
+FROM nexus.engageska-portugal.pt/ska-tango-images/pytango-runtime:9.3.3.3 AS runtime
 
 ARG IMAGE_VERSION=0.0.1
 
diff --git a/Makefile b/Makefile
index cf201f8e88087cc647c98ef98e33a134a1ad76ff..3326f5ca49537fb6ba188ebe679bdb71648c3d75 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@ SHELL = /bin/bash
 #
 DOCKER_REGISTRY_USER:=ska-telescope
 PROJECT = ska_tango_base
-IMAGE_FOR_DIAGRAMS = nexus.engageska-portugal.pt/ska-docker/ska-python-buildenv:9.3.3.1
+IMAGE_FOR_DIAGRAMS = nexus.engageska-portugal.pt/ska-tango-images/pytango-builder:9.3.3.3
 #
 # include makefile to pick up the standard Make targets, e.g., 'make build'
 # build, 'make push' docker push procedure, etc. The other Make targets
@@ -44,16 +44,17 @@ test-in-docker: build ## Build the docker image and run tests inside it.
 lint-in-docker: build ## Build the docker image and run lint inside it.
 	@docker run --rm $(IMAGE):$(VERSION) make lint
 
-generate-diagrams-in-docker: ## Build the docker image and generate state machine diagrams inside it.
-	@docker run --rm -v $(PWD):/diagrams $(IMAGE_FOR_DIAGRAMS) bash -c "cd /diagrams && make generate-diagrams-in-docker-internals"
+generate-diagrams-in-docker: ## Generate state machine diagrams using a container.
+	@docker run --rm -v $(PWD):/project $(IMAGE_FOR_DIAGRAMS) bash -c "cd /project && make generate-diagrams-in-docker-internals"
 
 generate-diagrams-in-docker-internals:  ## Generate state machine diagrams (within a container!)
 	test -f /.dockerenv  # ensure running in docker container
 	apt-get update
 	apt-get install --yes graphviz graphviz-dev gsfonts pkg-config
 	python3 -m pip install pygraphviz
-	cd /diagrams/docs/source && python3 draw_state_machines.py
-	ls -lo /diagrams/docs/source/images/
+	cd /project && python3 -m pip install .
+	cd /project/docs/source && python3 scripts/draw_state_machines.py
+	ls -lo /project/docs/source/api/*/*.png
 
 docs-in-docker: ## Generate docs inside a container
 	@docker build -t ska_tango_base_docs_builder  . -f docs/Dockerfile
diff --git a/README.md b/README.md
index 35836788cac3e18291b4381762c2fa9e602befdc..4a4e4d2b74d0fe0bd267f2116c8bf7a97b8cc792 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,10 @@
 # SKA Tango Base Classes and Utilities
 
-
-
 [![Documentation Status](https://readthedocs.org/projects/ska-tango-base/badge/?version=latest)](https://developerskatelescopeorg.readthedocs.io/projects/ska-tango-base/en/latest/?badge=latest)
 
-
-
 ## About
 
-A shared repository for the Local Monitoring and Control (LMC) Tango Base Classes. The goal is to create a Software Development Kit for the Control System of the [Square Kilometre Array](http://skatelescope.org/) (SKA) radio telescope project. The Telescope Manager provides the Central Control System and each _Element_ provides a Local Control System that all work together as the Control System for the instrument. In the SKA case _Elements_ are subsystems such as the Central Signal Processor (CSP), Science Data Processor (SDP), Dishes (DSH), Low-Frequency Apperture Array (LFAA) etc.  Control is implement using the distributed control system, [TANGO](http://www.tango-controls.org), which is accessed from Python using the [PyTango](https://gitlab.com/tango-controls/pytango) package.
-
+A shared repository for the Local Monitoring and Control (LMC) Tango Base Classes. The goal is to create a Software Development Kit for the Control System of the [Square Kilometre Array](http://skatelescope.org/) (SKA) radio telescope project. The Telescope Manager provides the Central Control System and each _Element_ provides a Local Control System that all work together as the Control System for the instrument. In the SKA case _Elements_ are subsystems such as the Central Signal Processor (CSP), Science Data Processor (SDP), Dishes (DSH), Low-Frequency Apperture Array (LFAA) etc.  Control is implement using the distributed control system, [Tango](http://www.tango-controls.org), which is accessed from Python using the [PyTango](https://gitlab.com/tango-controls/pytango) package.
 
 Early work in this repo was done as part of the LMC Base Classes Evolutionary Prototype (LEvPro) project, under the INDO-SA collaboration program.
 
@@ -23,8 +18,244 @@ The ska-tango-base repository includes a set of eight classes as mentioned in SK
 - SKASubarray: This is the generic base class which provides common functionality required in a subarray device.
 - SKATelState: This is the generic base class to provide common functionality of a TelState device of any SKA Element.
 
+
+## Instructions
+For detailed instructions on installation and usage, see the [Developers Guide](https://developer.skao.int/projects/ska-tango-base/en/latest/guide/index.html).
+
+### Installation
+#### Requirements
+The basic requirements are:
+- Python 3.5
+- Pip
+
+The requirements for installation of the lmc base classes are:
+- argparse
+
+The requirements for testing are:
+- coverage
+- pytest
+- pytest-cov
+- pytest-xdist
+- mock
+
+#### Installation steps
+1. Clone the repository on local machine.
+2. Navigate to the root directory of the repository from terminal
+3. Run 'python3 -m pip install . --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple'
+
+### Testing
+The project can be tested locally my invoking *make CI_JOB_ID=some_id test* command. This invokes a chain of commands from the makefile which builds the project's python package, creates a docker image with the project, instantiates separate container for each of the base class and runs unit test cases of each class. Additionally, code analysis is also done and code coverage report is prepared. After testing is done, the containers are taken down.
+
+### Usage
+The base classes are installed as a Python package in the system. The intended usage of the base classes is to inherit the class according to the requirement. The class needs to be imported in the module. e.g.
+```
+from ska.base import SKABaseDevice
+
+class DishLeafNode(SKABaseDevice):
+.
+.
+.
+```
+
+### Development
+
+#### PyCharm
+
+The Docker integration is recommended.  For development, use the `nexus.engageska-portugal.pt/ska-telescope/ska_tango_base:latest` image as the Python Interpreter for the project.  Note that if `make` is run with targets like `build`, `up`, or `test`, that image will be rebuilt by Docker using the local code, and tagged as `latest`.
+
+As this project uses a `src` [folder structure](https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure), so under _Preferences > Project Structure_, the `src` folder needs to be marked as "Sources".  That will allow the interpreter to be aware of the package from folders like `tests` that are outside of `src`. When adding Run/Debug configurations, make sure "Add content roots to PYTHONPATH" and "Add source roots to PYTHONPATH" are checked.
+
+### Docs
+- Online:  [Read The Docs](https://developer.skao.int/projects/ska-tango-base/en/latest/)
+
+### Contribute
+Contributions are always welcome! Please refer to the [SKA telescope developer portal](https://developer.skao.int/).
+
+## Logging explained
+
+In order to provided consistent logging across all Python Tango devices in SKA, the logging is configured in the LMC base class: `SKABaseDevice`.
+
+### Default logging targets
+
+The `SKABaseDevice` automatically uses the logging configuration provided by the [ska_logging](https://gitlab.com/ska-telescope/ska-logging) package.  This cannot be easily disabled, and should not be.  It allows us to get consistent logs from all devices, and to effect system wide change, if necessary.  Currently, that library sets up the root logger to always output to stdout (i.e., the console). This is so that the logs can be consumed by Fluentd and forwarded to Elastic.
+
+The way the `SKABaseDevice` logging formatter and filters are configured, the emitted logs include a tag field with the Tango device name.  This is useful when searching for logs in tools like Kibana.  For example, `tango-device=ska_mid/tm_leaf_node/d0004`. This should work even for multiple devices from a single device server.
+
+In addition, the `SKABaseDevice`'s default `loggingTargets` are configured to send all logs to the [Tango Logging Service](https://tango-controls.readthedocs.io/en/latest/development/device-api/device-server-writing.html#the-tango-logging-service) (TLS) as well.  The sections below explain the Tango device attributes and properties related to this.
+
+### Tango device controls for logging levels and targets
+
+The logging level and additional logging targets are controlled by two attributes. These attributes are initialised from two device properties on startup.  An extract of the definitions from the base class is shown below.
+
+As mentioned above, note that the `LoggingTargetsDefault` includes `"tango::logger"` which means Python logs are forwarded to the Tango Logging Service as well.  This can be overridden in the Tango database, however disabling the `"tango::logger"` target is strongly discouraged, as other devices in the telescope or test infrastructure may rely on this.
+
+```python
+class SKABaseDevice(Device):
+
+    ...
+
+    # -----------------
+    # Device Properties
+    # -----------------
+
+    LoggingLevelDefault = device_property(
+        dtype='uint16', default_value=LoggingLevel.INFO
+    )
+
+    LoggingTargetsDefault = device_property(
+        dtype='DevVarStringArray', default_value=["tango::logger"]
+    )
+
+    # ----------
+    # Attributes
+    # ----------
+
+    loggingLevel = attribute(
+        dtype=LoggingLevel,
+        access=AttrWriteType.READ_WRITE,
+        doc="Current logging level for this device - "
+            "initialises to LoggingLevelDefault on startup",
+    )
+
+    loggingTargets = attribute(
+        dtype=('str',),
+        access=AttrWriteType.READ_WRITE,
+        max_dim_x=4,
+        doc="Logging targets for this device, excluding ska_logging defaults"
+            " - initialises to LoggingTargetsDefault on startup",
+    )
+
+   ...
+
+```
+
+### Changing the logging level
+
+The `loggingLevel` attribute allows us to adjust the severity of logs being emitted. This attribute is an enumerated type.  The default is currently INFO level, but it can be overridden by setting the `LoggingLevelDefault` property in the Tango database.
+
+Example:
+```python
+proxy = tango.DeviceProxy('my/test/device')
+
+# change to debug level using an enum
+proxy.loggingLevel = ska.base.control_model.LoggingLevel.DEBUG
+
+# change to info level using a string
+proxy.loggingLevel = "INFO"
+```
+
+Do not use `proxy.set_logging_level()`.  That method only applies to the Tango Logging Service (see section below).  However, note that when the `loggingLevel` attribute is set, we internally update the TLS logging level as well.
+
+### Additional logging targets
+
+Note that the `loggingTargets` attribute says "excluding ska_logging defaults". Even when empty, you will still have the logging to stdout that is already provided by the ska_logging library.  If you want to forward logs to other targets, then you can use this attribute.  Since we also want logging to TLS, it should include the `"tango::logger"` item by default.
+
+The format and usage of this attribute is not that intuitive, but it was not expected to be used much, and was kept similar to the existing SKA control system guidelines proposal. The string format of each target is chosen to match that used by the Tango Logging Service: `"<type>::<location>"`.
+
+It is a spectrum string attribute.  In PyTango we read it back as a tuple of strings, and we can write it with either a list or tuple of strings.
+
+```python
+proxy = tango.DeviceProxy('my/test/device')
+
+# read back additional targets (as a tuple)
+current_targets = proxy.loggingTargets
+
+# add a new file target
+new_targets = list(current_targets) + ["file::/tmp/my.log"]
+proxy.loggingTargets = new_targets
+
+# disable all additional targets
+proxy.loggingTargets = []
+```
+
+Currently there are four types of targets implemented:
+- `console`
+- `file`
+- `syslog`
+- `tango`
+
+#### console target
+If you were to set the `proxy.loggingTargets = ["console::cout"]` you would get all the logs to stdout duplicated.  Once for ska_logging root logger, and once for the additional console logger you just added.  For the "console" option it doesn't matter what text comes after the `::` - we always use stdout.  While it may not seem useful now, the option is kept in case the ska_logging default configuration changes, and no longer outputs to stdout.
+
+#### file target
+For file output, provide the path after the `::`.  If the path is omitted, then a file is created in the device server's current directory, with a name based on the the Tango name.  E.g., "my/test/device" would get the file "my_test_device.log". Currently, we using a `logging.handlers.RotatingFileHandler` with a 1 MB limit and just 2 backups.  This could be modified in future.
+
+#### syslog target
+For syslog, the syslog target address details must be provided after the `::` as a URL. The following types are supported:
+- File, `file://<path>`
+  - E.g., for `/dev/log` use `file:///dev/log`.
+  - If the protocol is omitted, it is assumed to be `file://`.  Note: this is deprecated.
+    Support will be removed in v0.6.0.
+- Remote UDP server, `udp://<hostname>:<port>`
+  -  E.g., for `server.domain` on UDP port 514 use `udp://server.domain:514`.
+- Remote TCP server, `tcp://<hostname>:<port>`
+  -  E.g., for `server.domain` on TCP port 601 use `tcp://server.domain:601`.
+
+Example of usage:  `proxy.loggingTargets = ["syslog::udp://server.domain:514"]`.
+
+#### tango target
+All Python logs can be forwarded to the Tango Logging Service by adding the `"tango::logger"` target.  This will use the device's log4tango logger object to emit logs into TLS.  The TLS targets still need to be added in the usual way.  Typically, using the `add_logging_target` method from an instance of a `tango.DeviceProxy` object.
+
+#### multiple targets
+If you want file and syslog targets, you could do something like: `proxy.loggingTargets = ["file::/tmp/my.log", "syslog::udp://server.domain:514"]`.
+
+**Note:**  There is a limit of 4 additional handlers.  That is the maximum length of the spectrum attribute. We could change this if there is a reasonable use case for it.
+
+### Can I still send logs to the Tango Logging Service?
+
+Yes.  In `SKABaseDevice._init_logging` we monkey patch the log4tango logger methods `debug_stream`, `error_stream`, etc. to point the Python logger methods like `logger.debug`, `logger.error`, etc.  This means that logs are no longer forwarded to the Tango Logging Service automatically.  However, by including a `"tango::logger"` item in the `loggingTarget` attribute, the Python logs are sent to TLS.
+
+The `tango.DeviceProxy` also has some built in logging control methods that only apply to the Tango Logging Service:
+- `DeviceProxy.add_logging_target`
+  - Can be used to add a log consumer device.
+  - Can be used to log to file (in the TLS format).
+  - Should not be used to turn on console logging, as that will result in duplicate logs.
+- `DeviceProxy.remove_logging_target`
+  - Can be used to remove any TLS logging target.
+- `DeviceProxy.set_logging_level`
+  - Should not be used as it only applies to TLS.  The Python logger level will be out
+    of sync.  Rather use the device attribute `loggingLevel` which sets both.
+
+### Where are the logs from the admin device (dserver)?
+
+PyTango is wrapper around the C++ Tango library, and the admin device is implemented in C++. The admin device does not inherit from the SKABaseDevice and we cannot override its behaviour from the Python layer.  Its logs can only be seen by configuring the TLS appropriately.
+
+### What code should I write to log from my device?
+
+You should always use the `self.logger` object within methods.  This instance of the logger is the only one that knows the Tango device name.  You can also use the PyTango [logging decorators](https://pytango.readthedocs.io/en/stable/server_api/logging.html#logging-decorators) like `DebugIt`, since the monkey patching redirects them to that same logger.
+
+```python
+class MyDevice(SKABaseDevice):
+    def my_method(self):
+        someone = "you"
+        self.logger.info("I have a message for %s", someone)
+
+    @tango.DebugIt(show_args=True, show_ret=True)
+    def my_handler(self):
+        # great, entry and exit of this method is automatically logged
+        # at debug level!
+        pass
+
+```
+
+Yes, you could use f-strings. `f"I have a message for {someone}"`.  The only benefit of the `%s` type formatting is that the full string does not need to be created unless the log message will be emitted.  This could provide a small performance gain, depending on what is being logged, and how often.
+
+
+### When I set the logging level via command line it doesn't work
+
+Tango devices can be launched with a `-v` parameter to set the logging level. For example, 'MyDeviceServer instance -v5' for debug level.  Currently, the `SKABaseDevice` does not consider this command line option, so it will just use the Tango device property instead. In future, it would be useful to override the property with the command line option.
+
 ## Version History
 
+#### 0.11.0
+- Breaking change: state models and component managers
+  - Re-implementation of operational state model to better model
+    hardware and support device decoupling.
+  - Decoupling of state models from each other
+  - Introduction of component managers, to support component monitoring
+- Update to latest containers
+- Add developer guide to documentation
+
 #### 0.10.1
 - Make dependency on `pytango` and `numpy` python packages explicit.
 - Add optional "key" parameter to `SKASubarrayResourceManager` to filter JSON for
@@ -212,306 +443,3 @@ than only EMPTY obsState.
 - Repackaging of all the classes into a single Python package
 - Changes to folder structure,
 - Integration in CI environment
-
-## Installation
-### Requirements
-The basic requirements are:
-- Python 3.5
-- Pip
-
-The requirements for installation of the lmc bas classes are:
-- enum34
-- argparse
-- future
-
-The requirements for testing are:
-- coverage
-- pytest
-- pytest-cov
-- pytest-xdist
-- mock
-
-### Installation steps
-1. Clone the repository on local machine.
-2. Navigate to the root directory of the repository from terminal
-3. Run 'python3 -m pip install . --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple'
-
-## Testing
-The project can be tested locally my invoking *make CI_JOB_ID=some_id test* command.
-This invokes a chain of commands from the makefile which builds the project's
-python package, creates a docker image with the project, instantiates separate
-container for each of the base class and runs unit test cases of each class. Additionally,
-code analysis is also done and code coverage report is prepared.
-After testing is done, the containers are taken down.
-
-## Usage
-The base classes are installed as a Python package in the system. The intended usage of the base classes is to inherit the class according to the requirement. The class needs to be imported in the module. e.g.
-```
-from ska.base import SKABaseDevice
-
-class DishLeafNode(SKABaseDevice):
-.
-.
-.
-```
-
-## Logging explained
-
-In order to provided consistent logging across all Python Tango devices in SKA,
-the logging is configured in the LMC base class: `SKABaseDevice`.
-
-### Default logging targets
-
-The `SKABaseDevice` automatically uses the logging configuration provided by the
-[ska_logging](https://gitlab.com/ska-telescope/ska-logging) package.  This cannot
-be easily disabled, and should not be.  It allows us to get consistent logs from all
-devices, and to effect system wide change, if necessary.  Currently,
-that library sets up the root logger to always output to stdout (i.e., the console).
-This is so that the logs can be consumed by Fluentd and forwarded to Elastic.
-
-The way the `SKABaseDevice` logging formatter and filters are configured, the emitted
-logs include a tag field with the Tango device name.  This is useful when searching
-for logs in tools like Kibana.  For example, `tango-device=ska_mid/tm_leaf_node/d0004`.
-This should work even for multiple devices from a single device server.
-
-In addition, the `SKABaseDevice`'s default `loggingTargets` are configured to send all
-logs to the [Tango Logging Service](https://tango-controls.readthedocs.io/en/latest/development/device-api/device-server-writing.html#the-tango-logging-service)
-(TLS) as well.  The sections below explain the Tango device attributes and properties
-related to this.
-
-### Tango device controls for logging levels and targets
-
-The logging level and additional logging targets are controlled by two attributes.
-These attributes are initialised from two device properties on startup.  An extract
-of the definitions from the base class is shown below.
-
-As mentioned above, note that the `LoggingTargetsDefault` includes `"tango::logger"` which
-means Python logs are forwarded to the Tango Logging Service as well.  This can be overridden
-in the Tango database, however disabling the `"tango::logger"` target is strongly discouraged,
-as other devices in the telescope or test infrastructure may rely on this.
-
-```python
-class SKABaseDevice(Device):
-
-    ...
-
-    # -----------------
-    # Device Properties
-    # -----------------
-
-    LoggingLevelDefault = device_property(
-        dtype='uint16', default_value=LoggingLevel.INFO
-    )
-
-    LoggingTargetsDefault = device_property(
-        dtype='DevVarStringArray', default_value=["tango::logger"]
-    )
-
-    # ----------
-    # Attributes
-    # ----------
-
-    loggingLevel = attribute(
-        dtype=LoggingLevel,
-        access=AttrWriteType.READ_WRITE,
-        doc="Current logging level for this device - "
-            "initialises to LoggingLevelDefault on startup",
-    )
-
-    loggingTargets = attribute(
-        dtype=('str',),
-        access=AttrWriteType.READ_WRITE,
-        max_dim_x=4,
-        doc="Logging targets for this device, excluding ska_logging defaults"
-            " - initialises to LoggingTargetsDefault on startup",
-    )
-
-   ...
-
-```
-
-### Changing the logging level
-
-The `loggingLevel` attribute allows us to adjust the severity of logs being emitted.
-This attribute is an enumerated type.  The default is currently INFO level, but it
-can be overridden by setting the `LoggingLevelDefault` property in the Tango database.
-
-Example:
-```python
-proxy = tango.DeviceProxy('my/test/device')
-
-# change to debug level using an enum
-proxy.loggingLevel = ska.base.control_model.LoggingLevel.DEBUG
-
-# change to info level using a string
-proxy.loggingLevel = "INFO"
-```
-
-Do not use `proxy.set_logging_level()`.  That method only applies to the Tango Logging
-Service (see section below).  However, note that when the `loggingLevel` attribute
-is set, we internally update the TLS logging level as well.
-
-### Additional logging targets
-
-Note that the `loggingTargets` attribute says "excluding ska_logging defaults".
-Even when empty, you will still have the logging to stdout that is already provided
-by the ska_logging library.  If you want to forward logs to other targets, then you can use
-this attribute.  Since we also want logging to TLS, it should include the `"tango::logger"`
-item by default.
-
-The format and usage of this attribute is not that intuitive, but it was not expected to
-be used much, and was kept similar to the existing SKA control system guidelines proposal.
-The string format of each target is chosen to match that used by the
-Tango Logging Service: `"<type>::<location>"`.
-
-It is a spectrum string attribute.  In PyTango we read it back as a tuple of strings,
-and we can write it with either a list or tuple of strings.
-
-```python
-proxy = tango.DeviceProxy('my/test/device')
-
-# read back additional targets (as a tuple)
-current_targets = proxy.loggingTargets
-
-# add a new file target
-new_targets = list(current_targets) + ["file::/tmp/my.log"]
-proxy.loggingTargets = new_targets
-
-# disable all additional targets
-proxy.loggingTargets = []
-```
-
-Currently there are four types of targets implemented:
-- `console`
-- `file`
-- `syslog`
-- `tango`
-
-#### console target
-If you were to set the `proxy.loggingTargets = ["console::cout"]` you would get
-all the logs to stdout duplicated.  Once for ska_logging root logger, and once for
-the additional console logger you just added.  For the "console" option it doesn't matter
-what text comes after the `::` - we always use stdout.  While it may not seem useful
-now, the option is kept in case the ska_logging default configuration changes, and no
-longer outputs to stdout.
-
-#### file target
-For file output, provide the path after the `::`.  If the path is omitted, then a
-file is created in the device server's current directory, with a name based on the
-the Tango name.  E.g., "my/test/device" would get the file "my_test_device.log".
-Currently, we using a `logging.handlers.RotatingFileHandler` with a 1 MB limit and
-just 2 backups.  This could be modified in future.
-
-#### syslog target
-For syslog, the syslog target address details must be provided after the `::` as a URL.
-The following types are supported:
-- File, `file://<path>`
-  - E.g., for `/dev/log` use `file:///dev/log`.
-  - If the protocol is omitted, it is assumed to be `file://`.  Note: this is deprecated.
-    Support will be removed in v0.6.0.
-- Remote UDP server, `udp://<hostname>:<port>`
-  -  E.g., for `server.domain` on UDP port 514 use `udp://server.domain:514`.
-- Remote TCP server, `tcp://<hostname>:<port>`
-  -  E.g., for `server.domain` on TCP port 601 use `tcp://server.domain:601`.
-
-Example of usage:  `proxy.loggingTargets = ["syslog::udp://server.domain:514"]`.
-
-#### tango target
-All Python logs can be forwarded to the Tango Logging Service by adding the `"tango::logger"`
-target.  This will use the device's log4tango logger object to emit logs into TLS.  The
-TLS targets still need to be added in the usual way.  Typically, using the `add_logging_target`
-method from an instance of a `tango.DeviceProxy` object.
-
-#### multiple targets
-If you want file and syslog targets, you could do something like:
-`proxy.loggingTargets = ["file::/tmp/my.log", "syslog::udp://server.domain:514"]`.
-
-**Note:**  There is a limit of 4 additional handlers.  That is the maximum length
-of the spectrum attribute. We could change this if there is a reasonable use
-case for it.
-
-### Can I still send logs to the Tango Logging Service?
-
-Yes.  In `SKABaseDevice._init_logging` we monkey patch the log4tango logger
-methods `debug_stream`, `error_stream`, etc. to point the Python logger methods like
-`logger.debug`, `logger.error`, etc.  This means that logs are no longer forwarded
-to the Tango Logging Service automatically.  However, by including a `"tango::logger"`
-item in the `loggingTarget` attribute, the Python logs are sent to TLS.
-
-The `tango.DeviceProxy` also has some built in logging control methods that only apply
-to the Tango Logging Service:
-- `DeviceProxy.add_logging_target`
-  - Can be used to add a log consumer device.
-  - Can be used to log to file (in the TLS format).
-  - Should not be used to turn on console logging, as that will result in duplicate logs.
-- `DeviceProxy.remove_logging_target`
-  - Can be used to remove any TLS logging target.
-- `DeviceProxy.set_logging_level`
-  - Should not be used as it only applies to TLS.  The Python logger level will be out
-    of sync.  Rather use the device attribute `loggingLevel` which sets both.
-
-### Where are the logs from the admin device (dserver)?
-
-PyTango is wrapper around the C++ Tango library, and the admin device is implemented in C++.
-The admin device does not inherit from the SKABaseDevice and we cannot override its behaviour
-from the Python layer.  Its logs can only be seen by configuring the TLS appropriately.
-
-### What code should I write to log from my device?
-
-You should always use the `self.logger` object within methods.  This instance of the
-logger is the only one that knows the Tango device name.  You can also use the PyTango
-[logging decorators](https://pytango.readthedocs.io/en/stable/server_api/logging.html#logging-decorators)
-like `DebugIt`, since the monkey patching redirects them to that same logger.
-
-```python
-class MyDevice(SKABaseDevice):
-    def my_method(self):
-        someone = "you"
-        self.logger.info("I have a message for %s", someone)
-
-    @tango.DebugIt(show_args=True, show_ret=True)
-    def my_handler(self):
-        # great, entry and exit of this method is automatically logged
-        # at debug level!
-        pass
-
-```
-
-Yes, you could use f-strings. `f"I have a message for {someone}"`.  The only benefit
-of the `%s` type formatting is that the full string does not need to be created unless
-the log message will be emitted.  This could provide a small performance gain, depending
-on what is being logged, and how often.
-
-
-### When I set the logging level via command line it doesn't work
-
-Tango devices can be launched with a `-v` parameter to set the logging level. For example,
-'MyDeviceServer instance -v5' for debug level.  Currently, the `SKABaseDevice` does not
-consider this command line option, so it will just use the Tango device property instead.
-In future, it would be useful to override the property with the command line option.
-
-## Development
-
-### PyCharm
-
-The Docker integration is recommended.  For development, use the
-`nexus.engageska-portugal.pt/ska-telescope/ska_tango_base:latest` image
-as the Python Interpreter for the project.  Note that if `make` is
-run with targets like `build`, `up`, or `test`, that image will be
-rebuilt by Docker using the local code, and tagged as `latest`.
-
-As this project uses a `src` [folder structure](https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure),
-so under _Preferences > Project Structure_, the `src` folder needs to be marked as "Sources".  That will
-allow the interpreter to be aware of the package from folders like `tests` that are outside of `src`.
-When adding Run/Debug configurations, make sure "Add content roots to PYTHONPATH" and
-"Add source roots to PYTHONPATH" are checked.
-
-## Docs
-- Online:  [Read The Docs](https://developerskatelescopeorg.readthedocs.io/projects/ska-tango-base/en/latest)
-- SKA Control System guidelines:  [Google docs folder](https://drive.google.com/drive/folders/0B8fhAW5QnZQWQ2ZlcjhVS0NmRms)
-- Old LEvPro work area: [Google docs folder](https://drive.google.com/drive/folders/0B8fhAW5QnZQWVHVFVGVXT2Via28)
-
-
-
-## Contribute
-Contributions are always welcome! Please refer to the [SKA Developer Portal](https://developer.skatelescope.org/en/latest/).
diff --git a/docs/source/Commands.rst b/docs/source/Commands.rst
deleted file mode 100644
index 134d62cea2d943f8cf4325d2a61ed84fd3522a7c..0000000000000000000000000000000000000000
--- a/docs/source/Commands.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-
-Commands
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.commands
-    :members:
-    :undoc-members:
-
diff --git a/docs/source/Control_Model.rst b/docs/source/Control_Model.rst
deleted file mode 100644
index a0add5fb6ff62ffd03ef0301a8d964470e04f89f..0000000000000000000000000000000000000000
--- a/docs/source/Control_Model.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-SKA Control Model
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.control_model
-    :members:
-    :undoc-members:
-
diff --git a/docs/source/CspSubElementMaster.rst b/docs/source/CspSubElementMaster.rst
deleted file mode 100644
index 6128b98d0708f921b682c62a1ae1781ccd0cb7e2..0000000000000000000000000000000000000000
--- a/docs/source/CspSubElementMaster.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-CSP Sub-element Master
-============================================
-This module implements a general Master device for a CSP Sub-element.
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.csp_subelement_master
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.CspSubElementMaster
-   :members:
-   :undoc-members:
diff --git a/docs/source/CspSubElementObsDevice.rst b/docs/source/CspSubElementObsDevice.rst
deleted file mode 100644
index 859d6e2c9afb62a0369019887c3883172d3da445..0000000000000000000000000000000000000000
--- a/docs/source/CspSubElementObsDevice.rst
+++ /dev/null
@@ -1,68 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-.. |br| raw:: html
-
-   <br />
-
-CSP Sub-element ObsDevice
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.csp_subelement_obsdevice
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.CspSubElementObsDevice
-   :members:
-   :undoc-members:
-
-
-Instance attributes
--------------------
-Here it is reported the list of the *instance attributes*. |br|
-
-* ``scan_id``: the identification number of the scan. |br|
-  The scan ID is passed as argument of the *Scan* command. |br|
-  The attribute value is reported via TANGO attribute *scanID*.
-
-* ``_sdp_addresses``: a python dictionary with the SDP destination addresses for the output
-  products. |br|
-  Depending on the sub-element (CBF, PSS, PST) this attribute can specify more than one destination address,
-  as for example in CBF sub-element. |br|
-  The SDP destination addresses are specified at configuration.
-  An SDP address specifies the MAC address, IP address and port of the endpoint. |br|
-  Below an example of how SDP addresses are specified in a Mid CBF configuration::
-
-        {
-          ...
-          "outputHost": [[0, "192.168.0.1"], [8184, "192.168.0.2"]],
-          "outputMac": [[0, "06-00-00-00-00-01"]],
-          "outputPort": [[0, 9000, 1], [8184, 9000, 1]]
-          ...
-        }
-
-  The value of this attribute is reported via the TANGO *sdpDestionationAddresses* attribute.
-
-  .. note::  Not all the Sub-element observing devices are connected to the SDP (for example Mid VCCs).
-
-
-* ``_sdp_links_active``: a python list of boolean. Each list element reports the network connectivity of the
-  corresponding link to SDP.
-
-* ``_sdp_links_capacity``: this attribute records the capacity in GB/s of the SDP link.
-
-* ``_config_id``: it stores the unique identificator associated to a JSON scan configuration. |br|
-  The value of this attribute is reported via the TANGO attriute *configID*.
-
-* ``_last_scan_configuration``: this attribute stores the last configuration successully programmed. |br|
-  The value is reported via the TANGO attribute *lastScanConfiguration*.
-
-
-* ``_health_failure_msg``:
-  The value is reported via the TANGO attribute *healthFailureMesssage*.
diff --git a/docs/source/CspSubElementSubarray.rst b/docs/source/CspSubElementSubarray.rst
deleted file mode 100644
index 78863176de600da3130b3a53ae18048ecc60c8e8..0000000000000000000000000000000000000000
--- a/docs/source/CspSubElementSubarray.rst
+++ /dev/null
@@ -1,23 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-CSP Sub-element Subarray
-============================================
-This module implements a generic Subarray device for a CSP Sub-element.
-The scope of this module is to provide a uniform access to a CSP Sub-element
-subarray from the CSP.LMC side.
-
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.csp_subelement_subarray
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.CspSubElementSubarray
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKAAlarmHandler.rst b/docs/source/SKAAlarmHandler.rst
deleted file mode 100644
index fde7bbc1d1e0df57c563c30298e1919841e8fd74..0000000000000000000000000000000000000000
--- a/docs/source/SKAAlarmHandler.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Alarm Handler
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.alarm_handler_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKAAlarmHandler
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKABaseDevice.rst b/docs/source/SKABaseDevice.rst
deleted file mode 100644
index 6fe03c19cc9586b93720eebae52018b982d30a8e..0000000000000000000000000000000000000000
--- a/docs/source/SKABaseDevice.rst
+++ /dev/null
@@ -1,26 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Base Device
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.base_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKABaseDevice
-   :members:
-   :undoc-members:
-
-Device State Model
-------------------
-
-.. autoclass:: ska_tango_base.DeviceStateModel
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKACapability.rst b/docs/source/SKACapability.rst
deleted file mode 100644
index 1f465f8f459f543db1f25b6d6ec789ee71435c8a..0000000000000000000000000000000000000000
--- a/docs/source/SKACapability.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Capability
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.capability_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKACapability
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKALogger.rst b/docs/source/SKALogger.rst
deleted file mode 100644
index 1ef5114f7c83ca1aff253b8669f211027a483c74..0000000000000000000000000000000000000000
--- a/docs/source/SKALogger.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Logger
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.logger_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKALogger
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKAMaster.rst b/docs/source/SKAMaster.rst
deleted file mode 100644
index adfb37d2dcefb5a7aabfcb98710de2b4b73cf625..0000000000000000000000000000000000000000
--- a/docs/source/SKAMaster.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Master
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.master_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKAMaster
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKAObsDevice.rst b/docs/source/SKAObsDevice.rst
deleted file mode 100644
index 6f8322a9094ffa76cd61e967b6e6d4393174d0f3..0000000000000000000000000000000000000000
--- a/docs/source/SKAObsDevice.rst
+++ /dev/null
@@ -1,26 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Observation Device
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.obs_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKAObsDevice
-   :members:
-   :undoc-members:
-
-Device State Model
-------------------
-
-.. autoclass:: ska_tango_base.ObsDeviceStateModel
-   :members:
-   :undoc-members:
\ No newline at end of file
diff --git a/docs/source/SKASubarray.rst b/docs/source/SKASubarray.rst
deleted file mode 100644
index 7b4a3b6e06d82ed8204d14993a63bc9fdbdccbc1..0000000000000000000000000000000000000000
--- a/docs/source/SKASubarray.rst
+++ /dev/null
@@ -1,33 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Subarray
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.subarray_device
-
-Device Class
-------------
-
-.. autoclass:: ska_tango_base.SKASubarray
-   :members:
-   :undoc-members:
-
-Device State Model
-------------------
-
-.. autoclass:: ska_tango_base.SKASubarrayStateModel
-   :members:
-   :undoc-members:
-
-Resource Manager
-----------------
-
-.. autoclass:: ska_tango_base.SKASubarrayResourceManager
-   :members:
-   :undoc-members:
diff --git a/docs/source/SKATelState.rst b/docs/source/SKATelState.rst
deleted file mode 100644
index 4a1ba64678e39c53205cf961ba52065f86bbd397..0000000000000000000000000000000000000000
--- a/docs/source/SKATelState.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Telescope State
-============================================
-
-.. toctree::
-   :maxdepth: 2
-
-.. automodule:: ska_tango_base.tel_state_device
-
-Tango Device Class
-------------------
-
-.. autoclass:: ska_tango_base.SKATelState
-   :members:
-   :undoc-members:
diff --git a/docs/source/State_Machine.rst b/docs/source/State_Machine.rst
deleted file mode 100644
index b8133a507f0f3ffcdb6d5478db64b128a99b59db..0000000000000000000000000000000000000000
--- a/docs/source/State_Machine.rst
+++ /dev/null
@@ -1,192 +0,0 @@
-
-State Machine
-=============
-
-The state machine module implements three fundamental SKA state
-machines:
-
-* the admin mode state machine
-* the operational state (opState, represented in TANGO devices by TANGO
-  state) state machine
-* the observation state machine.
-
-Admin mode state machine
-------------------------
-The admin mode state machine allows for transitions between the five
-administrative modes:
-
-* NOT_FITTED: this is the lowest state of readiness, representing
-  devices that cannot be deployed without some external action, such as
-  plugging hardware in or updating network settings.)
-* RESERVED: the device is fitted but redundant to other devices. It is
-  ready to take over should other devices fail.
-* OFFLINE: the device has been declared by SKA operations not currently
-  to be used for operations (or whatever other function it provides)
-* MAINTENANCE: the device cannot be used for science purposes but can be
-  operationed for engineering / maintenance purposes, such as testing,
-  debugging, etc
-* ONLINE: the device can be used for science purposes.
-
-The admin mode state machine allows for
-
-* any transition between the modes NOT_FITTED, RESERVED and OFFLINE
-  (e.g. an unfitted device being fitted as a redundant or non-redundant
-  device, a redundant device taking over when another device fails, etc)
-* any transition between the modes OFFLINE, MAINTENANCE and ONLINE (e.g.
-  an online device being taken offline or put into maintenance mode to
-  diagnose a fault, a faulty device moving between maintenance and
-  offline mode as it undergoes sporadic periods of diagnosis.
-
-Diagrams of the admin mode state machine are shown below.
-
-.. uml:: adminMode_state_machine.uml
-  :caption: Diagram of the admin mode state machine, as designed
-
-.. figure:: images/AdminModeStateMachine_autogenerated.png
-  :alt: Diagram of the admin mode state machine, as implemented
-
-  Diagram of the admin mode state machine, automatically generated from
-  the implementation. The equivalence of this diagram to the diagram
-  above demonstrates that the machine has been implemented as designed.
-
-
-Operational state machine
--------------------------------
-The operational state (opState) machine represents the operational state
-of a SKA device. It is represented in TANGO devices using the TANGO
-"state", so the states used are a subset of the TANGO states: INIT,
-FAULT, DISABLE, STANDBY, OFF and ON.
-
-* INIT: the device is currently initialising
-* FAULT: the device has experienced an error from which it could not
-  recover.
-* DISABLE: the device is in its lowest state of readiness, from which
-  it may take some time to become fully operational. For example, if the
-  device manages hardware, that hardware may be switched off.
-* STANDBY: the device is unready, but can be made ready quickly. For
-  example, if the device manages hardware, that hardware may be in a
-  low-power standby mode.
-* OFF: the device is fully operational but is not currently in use
-* ON: the device is in use
-
-The operational state state machine allows for:
-
-* transition from INIT or FAULT into any of the three "readiness states"
-  DISABLE, STANDBY and OFF.
-* all transitions between these three "readiness states" DISABLE,
-  STANDBY and OFF.
-* transition between OFF and ON.
-
-.. figure:: images/OperationStateMachine_decoupled.png
-  :alt: Diagram of the operational state state machine, as designed,
-        ignoring coupling with admin mode
-
-  Diagram of the operational state (opState) state machine, as
-  designed, ignoring coupling with admin mode
-
-Unfortunately, operational state is inextricably coupled with admin
-mode: there are admin modes that imply disablement, and operational
-states such as ON should not be possible in such admin modes.
-
-To facilitate this, the entire operational state state machine is
-accessible only when the admin mode is ONLINE or MAINTENANCE. When in
-any other admin mode, the only permitted operational states are INIT,
-FAULT and DISABLE. This constraint is implemented into the operational
-state state machine by
-
-* three extra states: INIT_ADMIN, FAULT_ADMIN and DISABLED_ADMIN
-* two extra transition triggers: "admin_on" and "admin_off", which allow
-  for transition between INIT and INIT_ADMIN; FAULT and FAULT_ADMIN; and
-  DISABLE and DISABLE_ADMIN.
-
-This implementation minimises the coupling between admin mode and
-operational state, allowing the two machines to be conceptualised almost
-separately.
-
-Diagrams of the operational state state machine are shown below.
-
-.. figure:: images/OperationStateMachine_coupled.png
-  :alt: Diagram of the operational state state machine, as designed,
-        showing coupling with admin mode
-
-  Diagram of the operational state (opState) state machine, as
-  designed, showing coupling with admin mode
-
-.. figure:: images/OperationStateMachine_autogenerated.png
-  :width: 80%
-  :alt: Diagram of the operational state state machine, as implemented
-
-  Diagram of the operational state state machine, automatically
-  generated from the implementation. The equivalence of this diagram
-  to the diagram above demonstrates that the machine has been
-  implemented as designed.
-
-
-Observation state machine
--------------------------
-The observation state machine is implemented by devices that manage
-observations (currently only subarray devices).
-
-.. figure:: images/ObservationStateMachine_adr8.png
-  :width: 80%
-  :alt: Diagram of the observation state machine, as decided and
-        published in ADR-8.
-
-  Diagram of the observation state machine, as decided and published in
-  ADR-8.
-
-
-.. figure:: images/ObservationStateMachine_autogenerated.png
-  :width: 80%
-  :alt: Diagram of the observation state machine, automatically
-        generated from the implementation
-
-  Diagram of the observation state machine, automatically generated from
-  the implementation. The equivalance of this diagram to the diagram
-  previous demonstrates that the machine has been implemented in
-  conformance with ADR-8.
-
-CSP SubElement ObsDevice state machine
---------------------------------------------------
-This  state machine is implemented for the CSP SubElement devices, different
-from the subarrays, that manage observations.
-
-Compared to the SKA Observation State Machine, it implements a smaller number
-of states, number that can be further descreased depending on the necessities of the different
-sub-elements.
-
-The implemented states for the current state machine are:
-
-* IDLE: this is the observing state after the device intialization.
-* CONFIGURING: transitional state to report the device configuration is in progress.
-  *Need to understand if this state is really required by the observing devices of any
-  CSP sub-element.*
-* READY: the device is configured and is ready to perform observations
-* SCANNING: the device is performing the observation.
-* ABORTING: the device is processing an abort.
-  Need to understand if this state is really required by the observing devices of any
-  CSP sub-element.
-* ABORTED: the device has completed the abort request.
-* FAULT: the device has experienced an error from which it can be recovered only via
-  manual intervention invoking a reset command that force the device to the base
-  state (IDLE).
-
-.. figure:: images/CspSubElementObsDeviceStateMachine_autogenerated.png
-  :width: 80%
-  :alt: Diagram of the CSP SubElement observation state machine, automatically
-        generated from the implementation
-
-  Diagram of the CSP SubElement observation state machine, automatically generated from
-  the implementation.
-
-API
----
-
-.. automodule:: ska_tango_base.state_machine
-    :members:
-    :undoc-members:
-
-
-.. automodule:: ska_tango_base.csp_subelement_state_machine
-    :members:
-    :undoc-members:
diff --git a/docs/source/api/alarm_handler_device.rst b/docs/source/api/alarm_handler_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..be3a7d8f5902866d1a64da465d6367bfd64bc9e9
--- /dev/null
+++ b/docs/source/api/alarm_handler_device.rst
@@ -0,0 +1,6 @@
+====================
+Alarm Handler Device
+====================
+
+.. automodule:: ska_tango_base.alarm_handler_device
+   :members:
diff --git a/docs/source/api/base/_AdminModeMachine_autogenerated.png b/docs/source/api/base/_AdminModeMachine_autogenerated.png
new file mode 100644
index 0000000000000000000000000000000000000000..62f7193c645533f531e135100c3d513267d4c348
Binary files /dev/null and b/docs/source/api/base/_AdminModeMachine_autogenerated.png differ
diff --git a/docs/source/api/base/_OpStateMachine_autogenerated.png b/docs/source/api/base/_OpStateMachine_autogenerated.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c58b3855a283c3f0fc6027bb77be3b8e2f473f9
Binary files /dev/null and b/docs/source/api/base/_OpStateMachine_autogenerated.png differ
diff --git a/docs/source/api/base/admin_mode_model.rst b/docs/source/api/base/admin_mode_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9d1dd0ebe0093dd4d79537d8625ca6346f51021e
--- /dev/null
+++ b/docs/source/api/base/admin_mode_model.rst
@@ -0,0 +1,6 @@
+================
+Admin Mode Model
+================
+
+.. automodule:: ska_tango_base.base.admin_mode_model
+   :members:
diff --git a/docs/source/adminMode_state_machine.uml b/docs/source/api/base/admin_mode_model.uml
similarity index 100%
rename from docs/source/adminMode_state_machine.uml
rename to docs/source/api/base/admin_mode_model.uml
diff --git a/docs/source/api/base/base_device.rst b/docs/source/api/base/base_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8eb87944951700627a773e6b105010a5108b2f27
--- /dev/null
+++ b/docs/source/api/base/base_device.rst
@@ -0,0 +1,6 @@
+===========
+Base Device
+===========
+
+.. automodule:: ska_tango_base.base.base_device
+   :members:
diff --git a/docs/source/api/base/component_manager.rst b/docs/source/api/base/component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3071d88a8282903e511ffba7c715e97e1820310b
--- /dev/null
+++ b/docs/source/api/base/component_manager.rst
@@ -0,0 +1,6 @@
+======================
+Base Component Manager
+======================
+
+.. automodule:: ska_tango_base.base.component_manager
+   :members:
diff --git a/docs/source/api/base/index.rst b/docs/source/api/base/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ecfccb70e91a5a1a6f655ab03c15506cc1c3a92b
--- /dev/null
+++ b/docs/source/api/base/index.rst
@@ -0,0 +1,14 @@
+===============
+Base subpackage
+===============
+
+.. automodule:: ska_tango_base.base
+
+
+.. toctree::
+
+  Admin Mode Model<admin_mode_model>
+  Op State Model<op_state_model>
+  Base Component Manager<component_manager>
+  Reference Base Component Manager<reference_component_manager>
+  Base Device<base_device>
diff --git a/docs/source/api/base/op_state_machine.uml b/docs/source/api/base/op_state_machine.uml
new file mode 100644
index 0000000000000000000000000000000000000000..90cab54c39ca12052f76776a7c8939087388da19
--- /dev/null
+++ b/docs/source/api/base/op_state_machine.uml
@@ -0,0 +1,95 @@
+_UNINITIALISED: The Tango device has not yet\nstarted initialising.
+INIT_UNKNOWN: The Tango device is initialising\nand cannot determine\nthe state of its telescope component.
+INIT_DISABLE: The Tango device is initialising\nand is not monitoring\nits telescope component.
+INIT_OFF: The Tango device is initialising\nand the telescope component is turned off
+INIT_STANDBY: The Tango device is initialising\nand the telescope component is standing by
+INIT_ON: The Tango device is initialising\nand the telescope component is turned on
+INIT_FAULT: The Tango device is initialising\nand the telescope component has faulted
+
+UNKNOWN: The Tango device cannot determine\nthe state of its telescope component.
+DISABLE: The Tango device is not monitoring\nits telescope component.
+OFF: The telescope component is turned off
+STANDBY: The telescope component is standing by
+ON: The telescope component is turned on
+FAULT: The telescope component has faulted
+
+_UNINITIALISED --down--> INIT_UNKNOWN: init_invoked
+
+INIT_DISABLE --down--> DISABLE: init_completed
+INIT_UNKNOWN --down--> UNKNOWN: init_completed
+INIT_OFF --down--> OFF: init_completed
+INIT_STANDBY --down--> STANDBY: init_completed
+INIT_ON --down--> ON: init_completed
+INIT_FAULT --down--> FAULT: init_completed
+
+INIT_DISABLE -> INIT_UNKNOWN
+INIT_DISABLE -> INIT_OFF
+INIT_DISABLE -> INIT_STANDBY
+INIT_DISABLE -> INIT_ON
+INIT_DISABLE -> INIT_FAULT
+
+INIT_UNKNOWN -> INIT_DISABLE
+INIT_UNKNOWN -> INIT_OFF
+INIT_UNKNOWN -> INIT_STANDBY
+INIT_UNKNOWN -> INIT_ON
+INIT_UNKNOWN -> INIT_FAULT
+
+INIT_OFF -> INIT_DISABLE
+INIT_OFF -> INIT_UNKNOWN
+INIT_OFF -> INIT_STANDBY
+INIT_OFF -> INIT_ON
+INIT_OFF -> INIT_FAULT
+
+INIT_STANDBY -> INIT_DISABLE
+INIT_STANDBY -> INIT_UNKNOWN
+INIT_STANDBY -> INIT_OFF
+INIT_STANDBY -> INIT_ON
+INIT_STANDBY -> INIT_FAULT
+
+INIT_ON -> INIT_DISABLE
+INIT_ON -> INIT_UNKNOWN
+INIT_ON -> INIT_OFF
+INIT_ON -> INIT_STANDBY
+INIT_ON -> INIT_FAULT
+
+INIT_FAULT -> INIT_DISABLE
+INIT_FAULT -> INIT_UNKNOWN
+INIT_FAULT -> INIT_OFF
+INIT_FAULT -> INIT_STANDBY
+INIT_FAULT -> INIT_ON
+
+DISABLE -> UNKNOWN
+DISABLE -> OFF
+DISABLE -> STANDBY
+DISABLE -> ON
+DISABLE -> FAULT
+
+UNKNOWN -> DISABLE
+UNKNOWN -> OFF
+UNKNOWN -> STANDBY
+UNKNOWN -> ON
+UNKNOWN -> FAULT
+
+OFF -> DISABLE
+OFF -> UNKNOWN
+OFF -> STANDBY
+OFF -> ON
+OFF -> FAULT
+
+STANDBY -> DISABLE
+STANDBY -> UNKNOWN
+STANDBY -> OFF
+STANDBY -> ON
+STANDBY -> FAULT
+
+ON -> DISABLE
+ON -> UNKNOWN
+ON -> OFF
+ON -> STANDBY
+ON -> FAULT
+
+FAULT -> DISABLE
+FAULT -> UNKNOWN
+FAULT -> OFF
+FAULT -> STANDBY
+FAULT -> ON
diff --git a/docs/source/api/base/op_state_model.rst b/docs/source/api/base/op_state_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3fb7d60a7148e0b83272fc49b006f71efe1a0dc6
--- /dev/null
+++ b/docs/source/api/base/op_state_model.rst
@@ -0,0 +1,6 @@
+==============
+Op State Model
+==============
+
+.. automodule:: ska_tango_base.base.op_state_model
+   :members:
diff --git a/docs/source/api/base/op_state_model.uml b/docs/source/api/base/op_state_model.uml
new file mode 100644
index 0000000000000000000000000000000000000000..2ffc903ae85cf6ce02cd708e7da72d662f2247e7
--- /dev/null
+++ b/docs/source/api/base/op_state_model.uml
@@ -0,0 +1,49 @@
+INIT: The Tango device is initialising.
+UNKNOWN: The Tango device cannot determine\nthe state of its telescope component.
+DISABLE: The Tango device is not monitoring\nits telescope component.
+OFF: The telescope component is turned off
+STANDBY: The telescope component is standing by
+ON: The telescope component is turned on
+FAULT: The telescope component has faulted
+
+INIT --down--> DISABLE
+INIT --down--> UNKNOWN
+INIT --down--> OFF
+INIT --down--> STANDBY
+INIT --down--> ON
+INIT --down--> FAULT
+DISABLE -> UNKNOWN
+DISABLE -> OFF
+DISABLE -> STANDBY
+DISABLE -> ON
+DISABLE -> FAULT
+
+UNKNOWN -> DISABLE
+UNKNOWN -> OFF
+UNKNOWN -> STANDBY
+UNKNOWN -> ON
+UNKNOWN -> FAULT
+
+OFF -> DISABLE
+OFF -> UNKNOWN
+OFF -> STANDBY
+OFF -> ON
+OFF -> FAULT
+
+STANDBY -> DISABLE
+STANDBY -> UNKNOWN
+STANDBY -> OFF
+STANDBY -> ON
+STANDBY -> FAULT
+
+ON -> DISABLE
+ON -> UNKNOWN
+ON -> OFF
+ON -> STANDBY
+ON -> FAULT
+
+FAULT -> DISABLE
+FAULT -> UNKNOWN
+FAULT -> OFF
+FAULT -> STANDBY
+FAULT -> ON
diff --git a/docs/source/api/base/op_state_model_hierarchical.uml b/docs/source/api/base/op_state_model_hierarchical.uml
new file mode 100644
index 0000000000000000000000000000000000000000..67fa908a897f0f5bac235a84a4267a7dc41e755c
--- /dev/null
+++ b/docs/source/api/base/op_state_model_hierarchical.uml
@@ -0,0 +1,35 @@
+INIT: The Tango device is initialising.
+UNKNOWN: The Tango device cannot determine\nthe state of its telescope component.
+DISABLE: The Tango device is not monitoring\nits telescope component.
+
+INIT -right-> DISABLE
+INIT -right-> MONITORING
+INIT -right-> UNKNOWN
+DISABLE -down-> UNKNOWN
+DISABLE -down-> MONITORING
+UNKNOWN -up-> DISABLE
+UNKNOWN -down-> MONITORING
+MONITORING -up-> DISABLE
+MONITORING -up-> UNKNOWN
+
+state "[monitoring]" as MONITORING {
+  MONITORING: The Tango device is monitoring the telescope component.
+
+  OFF: The telescope component is turned off
+  STANDBY: The telescope component is standing by
+  ON: The telescope component is turned on
+  FAULT: The telescope component has faulted
+
+OFF -right-> STANDBY
+OFF -right-> FAULT
+OFF -right-> ON
+STANDBY -left-> OFF
+STANDBY -down-> ON
+STANDBY -down-> FAULT
+ON -left-> OFF
+ON -up-> STANDBY
+ON -down-> FAULT
+FAULT -left-> OFF
+FAULT -up-> STANDBY
+FAULT -up-> ON
+}
diff --git a/docs/source/api/base/reference_component_manager.rst b/docs/source/api/base/reference_component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d70135f333ba413828ed8e9c9bd08ffb5470cd8c
--- /dev/null
+++ b/docs/source/api/base/reference_component_manager.rst
@@ -0,0 +1,6 @@
+================================
+Reference Base Component Manager
+================================
+
+.. automodule:: ska_tango_base.base.reference_component_manager
+   :members:
diff --git a/docs/source/api/capability_device.rst b/docs/source/api/capability_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..52b8fb79003a8be166a55f7ff7d627a341ac9efd
--- /dev/null
+++ b/docs/source/api/capability_device.rst
@@ -0,0 +1,6 @@
+=================
+Capability Device
+=================
+
+.. automodule:: ska_tango_base.capability_device
+   :members:
diff --git a/docs/source/api/commands.rst b/docs/source/api/commands.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7a251c81334aba5fad8561db46f05693103bcef0
--- /dev/null
+++ b/docs/source/api/commands.rst
@@ -0,0 +1,6 @@
+========
+Commands
+========
+
+.. automodule:: ska_tango_base.commands
+   :members:
diff --git a/docs/source/api/control_model.rst b/docs/source/api/control_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..836668d3d8af3beb100e587da81a4dfde9159f6c
--- /dev/null
+++ b/docs/source/api/control_model.rst
@@ -0,0 +1,6 @@
+=============
+Control Model
+=============
+
+.. automodule:: ska_tango_base.control_model
+   :members:
diff --git a/docs/source/api/csp/index.rst b/docs/source/api/csp/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d0da45933e9e73fb17401149b083a6500fdd8433
--- /dev/null
+++ b/docs/source/api/csp/index.rst
@@ -0,0 +1,20 @@
+==============
+CSP subpackage
+==============
+
+.. automodule:: ska_tango_base.csp
+
+
+.. toctree::
+  :caption: Subpackages
+  :maxdepth: 2
+
+  CSP Obs subpackage<obs/index>
+  CSP Subarray subpackage<subarray/index>
+
+
+.. toctree::
+  :caption: Other modules
+  :maxdepth: 2
+
+  Master device<master_device>
diff --git a/docs/source/api/csp/master_device.rst b/docs/source/api/csp/master_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..75bdaa7ee7359cd68732a640808820cb37f85327
--- /dev/null
+++ b/docs/source/api/csp/master_device.rst
@@ -0,0 +1,6 @@
+============================
+CSP Subelement Master device
+============================
+
+.. automodule:: ska_tango_base.csp.master_device
+   :members:
diff --git a/docs/source/api/csp/obs/_CspSubElementObsStateMachine_autogenerated.png b/docs/source/api/csp/obs/_CspSubElementObsStateMachine_autogenerated.png
new file mode 100644
index 0000000000000000000000000000000000000000..756b134248a3b60af03e573cc35ac9a1d3b40f3b
Binary files /dev/null and b/docs/source/api/csp/obs/_CspSubElementObsStateMachine_autogenerated.png differ
diff --git a/docs/source/api/csp/obs/component_manager.rst b/docs/source/api/csp/obs/component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d082d253f7e8141db303a3faa370198741531cd7
--- /dev/null
+++ b/docs/source/api/csp/obs/component_manager.rst
@@ -0,0 +1,6 @@
+=========================
+CSP obs component manager
+=========================
+
+.. automodule:: ska_tango_base.csp.obs.component_manager
+   :members:
diff --git a/docs/source/api/csp/obs/index.rst b/docs/source/api/csp/obs/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..40dc1f2d019a67c82128edf498d4e756c9ba2393
--- /dev/null
+++ b/docs/source/api/csp/obs/index.rst
@@ -0,0 +1,13 @@
+==================
+CSP obs subpackage
+==================
+
+.. automodule:: ska_tango_base.csp.obs
+
+
+.. toctree::
+
+  CSP Obs State Model<obs_state_model>
+  CSP Obs Component Manager<component_manager>
+  Reference CSP Obs Component Manager<reference_component_manager>
+  CSP Obs device<obs_device>
diff --git a/docs/source/api/csp/obs/obs_device.rst b/docs/source/api/csp/obs/obs_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d556e02dac2bd49ebe6e67bd014d94aa77d46024
--- /dev/null
+++ b/docs/source/api/csp/obs/obs_device.rst
@@ -0,0 +1,6 @@
+=========================
+CSP Subelement Obs device
+=========================
+
+.. automodule:: ska_tango_base.csp.obs.obs_device
+   :members:
diff --git a/docs/source/api/csp/obs/obs_state_machine.uml b/docs/source/api/csp/obs/obs_state_machine.uml
new file mode 100644
index 0000000000000000000000000000000000000000..de2a62099e974bcbf81640fbc1cc722462e823ce
--- /dev/null
+++ b/docs/source/api/csp/obs/obs_state_machine.uml
@@ -0,0 +1,29 @@
+IDLE: The subarray is unconfigured
+CONFIGURING_IDLE: The subarray is unconfigured;\nis performing a configuring operation
+CONFIGURING_READY: The subarray is configured;\nis performing a configuring operation
+READY: The subarray is configured
+SCANNING: The subarray is scanning
+ABORTING: The subarray is aborting
+ABORTED: The subarray has aborted
+RESETTING: The subarray is resetting to IDLE\nfrom FAULT or ABORTED state
+FAULT: The subarray has faulted
+
+IDLE -down-> CONFIGURING_IDLE: configure_invoked
+IDLE -left-> ABORTING: abort_invoked
+CONFIGURING_IDLE -up-> IDLE: configure_completed
+CONFIGURING_IDLE -down-> CONFIGURING_READY: component_configured
+CONFIGURING_IDLE -left-> ABORTING: abort_invoked
+CONFIGURING_READY -down-> READY: configure_completed
+CONFIGURING_READY -left-> ABORTING: abort_invoked
+READY -up-> CONFIGURING_READY: configure_invoked
+READY -up-> IDLE: component_unconfigured
+READY -down-> SCANNING: component_scanning
+READY -left-> ABORTING: abort_invoked
+SCANNING -up-> READY: component_not_scanning
+SCANNING -left-> ABORTING: abort_invoked
+ABORTING -up-> ABORTED: abort_completed
+ABORTED -up-> RESETTING: obsreset_invoked
+RESETTING -down-> ABORTING: abort_invoked
+RESETTING -right-> IDLE: obsreset_completed
+[*] -up-> FAULT: component_obsfault\n(from any state)
+FAULT -up-> RESETTING: obsreset_invoked
diff --git a/docs/source/api/csp/obs/obs_state_model.rst b/docs/source/api/csp/obs/obs_state_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8391d0635e7a3bef4b857e548f8bd29f2bd842b7
--- /dev/null
+++ b/docs/source/api/csp/obs/obs_state_model.rst
@@ -0,0 +1,6 @@
+==============================
+CSP Subelement Obs State Model
+==============================
+
+.. automodule:: ska_tango_base.csp.obs.obs_state_model
+   :members:
diff --git a/docs/source/api/csp/obs/obs_state_model.uml b/docs/source/api/csp/obs/obs_state_model.uml
new file mode 100644
index 0000000000000000000000000000000000000000..9105b0dc27ed0acae1ba27e060805c76f5812a67
--- /dev/null
+++ b/docs/source/api/csp/obs/obs_state_model.uml
@@ -0,0 +1,27 @@
+IDLE: The subarray is unconfigured
+CONFIGURING: The subarray is performing a configuring operation
+READY: The subarray is configured
+SCANNING: The subarray is scanning
+ABORTING: The subarray is aborting
+ABORTED: The subarray has aborted
+RESETTING: The subarray is resetting to IDLE\nfrom FAULT or ABORTED state
+FAULT: The subarray has faulted
+
+IDLE -down-> CONFIGURING: configure_invoked
+IDLE -left-> ABORTING: abort_invoked
+CONFIGURING -up-> IDLE: configure_completed
+CONFIGURING -left-> ABORTING: abort_invoked
+CONFIGURING -down-> READY: configure_completed
+CONFIGURING -left-> ABORTING: abort_invoked
+READY -up-> CONFIGURING: configure_invoked
+READY -up-> IDLE: component_unconfigured
+READY -down-> SCANNING: component_scanning
+READY -left-> ABORTING: abort_invoked
+SCANNING -up-> READY: component_not_scanning
+SCANNING -left-> ABORTING: abort_invoked
+ABORTING -up-> ABORTED: abort_completed
+ABORTED -up-> RESETTING: obsreset_invoked
+RESETTING -down-> ABORTING: abort_invoked
+RESETTING -right-> IDLE: obsreset_completed
+[*] -up-> FAULT: component_obsfault\n(from any state)
+FAULT -up-> RESETTING: obsreset_invoked
diff --git a/docs/source/api/csp/obs/reference_component_manager.rst b/docs/source/api/csp/obs/reference_component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..beb98715b639bc7da4547109ee969328b91c80b2
--- /dev/null
+++ b/docs/source/api/csp/obs/reference_component_manager.rst
@@ -0,0 +1,6 @@
+===================================
+Reference CSP Obs Component Manager
+===================================
+
+.. automodule:: ska_tango_base.csp.obs.reference_component_manager
+   :members:
diff --git a/docs/source/api/csp/subarray/component_manager.rst b/docs/source/api/csp/subarray/component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f9e055256dabbe135d4dc094cf811d03aae72122
--- /dev/null
+++ b/docs/source/api/csp/subarray/component_manager.rst
@@ -0,0 +1,6 @@
+==============================
+CSP Subarray Component Manager
+==============================
+
+.. automodule:: ska_tango_base.csp.subarray.component_manager
+   :members:
diff --git a/docs/source/api/csp/subarray/index.rst b/docs/source/api/csp/subarray/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..dcb4bc5dae7497c7546c4d3dbf949716a584f103
--- /dev/null
+++ b/docs/source/api/csp/subarray/index.rst
@@ -0,0 +1,12 @@
+=======================
+CSP subarray subpackage
+=======================
+
+.. automodule:: ska_tango_base.csp.subarray
+
+
+.. toctree::
+
+  CSP Subarray Component Manager<component_manager>
+  Reference CSP Subarray Component Manager<reference_component_manager>
+  CSP Subarray device<subarray_device>
diff --git a/docs/source/api/csp/subarray/reference_component_manager.rst b/docs/source/api/csp/subarray/reference_component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4aecd76bbf5389ed2fc4e99ab9ff8a793847384e
--- /dev/null
+++ b/docs/source/api/csp/subarray/reference_component_manager.rst
@@ -0,0 +1,6 @@
+========================================
+Reference CSP Subarray Component Manager
+========================================
+
+.. automodule:: ska_tango_base.csp.subarray.reference_component_manager
+   :members:
diff --git a/docs/source/api/csp/subarray/subarray_device.rst b/docs/source/api/csp/subarray/subarray_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c0e7eefac20c1f600e36b095877c7bec56f6141b
--- /dev/null
+++ b/docs/source/api/csp/subarray/subarray_device.rst
@@ -0,0 +1,6 @@
+===================
+CSP Subarray device
+===================
+
+.. automodule:: ska_tango_base.csp.subarray.subarray_device
+   :members:
diff --git a/docs/source/api/faults.rst b/docs/source/api/faults.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c7701a00a501d1e5639f7f6ced76f0ea8e7837e2
--- /dev/null
+++ b/docs/source/api/faults.rst
@@ -0,0 +1,6 @@
+======
+Faults
+======
+
+.. automodule:: ska_tango_base.faults
+   :members:
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..7e57486ff398ed64b98af1bca195e85fd10b1134
--- /dev/null
+++ b/docs/source/api/index.rst
@@ -0,0 +1,34 @@
+===
+API
+===
+
+.. toctree::
+  :caption: Subpackages
+  :maxdepth: 2
+
+  Base subpackage<base/index>
+  Obs subpackage<obs/index>
+  CSP subpackage<csp/index>
+  Subarray subpackage<subarray/index>
+
+
+.. toctree::
+  :caption: Other devices
+  :maxdepth: 2
+
+  Alarm Handler device<alarm_handler_device>
+  Capability device<capability_device>
+  Logger device<logger_device>
+  Master device<master_device>
+  Tel State device<tel_state_device>
+
+
+.. toctree::
+  :caption: Other modules
+  :maxdepth: 2
+
+  Commands<commands>
+  Control Model<control_model>
+  Faults<faults>
+  Release<release>
+  Utils<utils>
diff --git a/docs/source/api/logger_device.rst b/docs/source/api/logger_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f91ffe08b4e48df32d599f26469534ab4e494a95
--- /dev/null
+++ b/docs/source/api/logger_device.rst
@@ -0,0 +1,6 @@
+=============
+Logger Device
+=============
+
+.. automodule:: ska_tango_base.logger_device
+   :members:
diff --git a/docs/source/api/master_device.rst b/docs/source/api/master_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ec74fa4c2121398bd6e034620fa664a7daa253c6
--- /dev/null
+++ b/docs/source/api/master_device.rst
@@ -0,0 +1,6 @@
+=============
+Master Device
+=============
+
+.. automodule:: ska_tango_base.master_device
+   :members:
diff --git a/docs/source/images/ObservationStateMachine_adr8.png b/docs/source/api/obs/ObservationStateMachine_adr8.png
similarity index 100%
rename from docs/source/images/ObservationStateMachine_adr8.png
rename to docs/source/api/obs/ObservationStateMachine_adr8.png
diff --git a/docs/source/api/obs/index.rst b/docs/source/api/obs/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5bfd51a324eb6bce696dddaa841a7262142293a7
--- /dev/null
+++ b/docs/source/api/obs/index.rst
@@ -0,0 +1,11 @@
+==============
+Obs subpackage
+==============
+
+.. automodule:: ska_tango_base.obs
+
+
+.. toctree::
+
+  Obs State Model<obs_state_model>
+  Obs Device<obs_device>
diff --git a/docs/source/api/obs/obs_device.rst b/docs/source/api/obs/obs_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..03133428dfa87a80c1a321b3ca7976a73dda458a
--- /dev/null
+++ b/docs/source/api/obs/obs_device.rst
@@ -0,0 +1,6 @@
+==========
+Obs Device
+==========
+
+.. automodule:: ska_tango_base.obs.obs_device
+   :members:
diff --git a/docs/source/api/obs/obs_state_model.rst b/docs/source/api/obs/obs_state_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..03424339409670c8a1afbfb69701206cc0f517d6
--- /dev/null
+++ b/docs/source/api/obs/obs_state_model.rst
@@ -0,0 +1,6 @@
+===============
+Obs State Model
+===============
+
+.. automodule:: ska_tango_base.obs.obs_state_model
+   :members:
diff --git a/docs/source/api/release.rst b/docs/source/api/release.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a7576acee06191d47556e95c9b6ee6203697ccd7
--- /dev/null
+++ b/docs/source/api/release.rst
@@ -0,0 +1,6 @@
+=======
+Release
+=======
+
+.. automodule:: ska_tango_base.release
+   :members:
diff --git a/docs/source/api/subarray/_SubarrayObsStateMachine_autogenerated.png b/docs/source/api/subarray/_SubarrayObsStateMachine_autogenerated.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1d8cb447fb4f7965ec952fe3281c56e464011ad
Binary files /dev/null and b/docs/source/api/subarray/_SubarrayObsStateMachine_autogenerated.png differ
diff --git a/docs/source/api/subarray/component_manager.rst b/docs/source/api/subarray/component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d1d83e3cd68e9b6a4582dc9ae8ae50acfd4720ef
--- /dev/null
+++ b/docs/source/api/subarray/component_manager.rst
@@ -0,0 +1,6 @@
+==========================
+Subarray Component Manager
+==========================
+
+.. automodule:: ska_tango_base.subarray.component_manager
+   :members:
diff --git a/docs/source/api/subarray/index.rst b/docs/source/api/subarray/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..58a07a06a88de1d94ec6e90869eebcd9c5a425e3
--- /dev/null
+++ b/docs/source/api/subarray/index.rst
@@ -0,0 +1,13 @@
+========
+Subarray
+========
+
+.. automodule:: ska_tango_base.subarray
+
+
+.. toctree::
+
+  Subarray Obs State Model<subarray_obs_state_model>
+  Subarray Component Manager<component_manager>
+  Reference Subarray Component Manager<reference_component_manager>
+  Subarray Device<subarray_device>
diff --git a/docs/source/api/subarray/reference_component_manager.rst b/docs/source/api/subarray/reference_component_manager.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b39b16889dcfb71e2431ce819e58883e154385ad
--- /dev/null
+++ b/docs/source/api/subarray/reference_component_manager.rst
@@ -0,0 +1,6 @@
+====================================
+Reference Subarray Component Manager
+====================================
+
+.. automodule:: ska_tango_base.subarray.reference_component_manager
+   :members:
diff --git a/docs/source/api/subarray/subarray_device.rst b/docs/source/api/subarray/subarray_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..881e7dd627ca7b434ef921c529bed568dacc30f2
--- /dev/null
+++ b/docs/source/api/subarray/subarray_device.rst
@@ -0,0 +1,6 @@
+===============
+Subarray Device
+===============
+
+.. automodule:: ska_tango_base.subarray.subarray_device
+   :members:
diff --git a/docs/source/api/subarray/subarray_obs_state_machine.uml b/docs/source/api/subarray/subarray_obs_state_machine.uml
new file mode 100644
index 0000000000000000000000000000000000000000..0bd016b893fd6e6cdd88a7e64e3e11697be7c6b4
--- /dev/null
+++ b/docs/source/api/subarray/subarray_obs_state_machine.uml
@@ -0,0 +1,45 @@
+EMPTY: The subarray has no resources
+RESOURCING_EMPTY: The subarray has no resources;\nis performing a resourcing operation
+RESOURCING_IDLE: The subarray has resources;\nis performing a resourcing operation
+IDLE: The subarray is unconfigured
+CONFIGURING_IDLE: The subarray is unconfigured;\nis performing a configuring operation
+CONFIGURING_READY: The subarray is configured;\nis performing a configuring operation
+READY: The subarray is configured
+SCANNING: The subarray is scanning
+ABORTING: The subarray is aborting
+ABORTED: The subarray has aborted
+RESETTING: The subarray is resetting to IDLE\nfrom FAULT or ABORTED state
+RESTARTING: The subarray is restarting to EMPTY\nfrom FAULT or ABORTED state
+FAULT: The subarray has faulted
+
+EMPTY -down-> RESOURCING_EMPTY: assign_invoked
+RESOURCING_EMPTY -up-> EMPTY: assign_completed
+RESOURCING_EMPTY -up-> EMPTY: release_completed
+RESOURCING_EMPTY -down-> RESOURCING_IDLE: component_resourced
+RESOURCING_IDLE -up-> RESOURCING_EMPTY: component_unresourced
+RESOURCING_IDLE -down-> IDLE: assign_completed
+RESOURCING_IDLE -down-> IDLE: release_completed
+IDLE -up-> RESOURCING_IDLE: assign_invoked
+IDLE -up-> RESOURCING_IDLE: release_invoked
+IDLE -down-> CONFIGURING_IDLE: configure_invoked
+IDLE -left-> ABORTING: abort_invoked
+CONFIGURING_IDLE -up-> IDLE: configure_completed
+CONFIGURING_IDLE -down-> CONFIGURING_READY: component_configured
+CONFIGURING_IDLE -left-> ABORTING: abort_invoked
+CONFIGURING_READY -down-> READY: configure_completed
+CONFIGURING_READY -left-> ABORTING: abort_invoked
+READY -up-> CONFIGURING_READY: configure_invoked
+READY -up-> IDLE: component_unconfigured
+READY -down-> SCANNING: component_scanning
+READY -left-> ABORTING: abort_invoked
+SCANNING -up-> READY: component_not_scanning
+SCANNING -left-> ABORTING: abort_invoked
+ABORTING -up-> ABORTED: abort_completed
+ABORTED -up-> RESETTING: obsreset_invoked
+ABORTED -up-> RESTARTING: restart_invoked
+RESETTING -down-> ABORTING: abort_invoked
+RESETTING -right-> IDLE: obsreset_completed
+RESTARTING -right-> EMPTY: restart_completed
+[*] -up-> FAULT: component_obsfault\n(from any state)
+FAULT -up-> RESETTING: obsreset_invoked
+FAULT -up-> RESTARTING: restart_invoked
diff --git a/docs/source/api/subarray/subarray_obs_state_model.rst b/docs/source/api/subarray/subarray_obs_state_model.rst
new file mode 100644
index 0000000000000000000000000000000000000000..ec0fec9e9ced31ce58cb973fe7c857396cd9d2e4
--- /dev/null
+++ b/docs/source/api/subarray/subarray_obs_state_model.rst
@@ -0,0 +1,6 @@
+========================
+Subarray Obs State Model
+========================
+
+.. automodule:: ska_tango_base.subarray.subarray_obs_state_model
+   :members:
diff --git a/docs/source/api/subarray/subarray_obs_state_model.uml b/docs/source/api/subarray/subarray_obs_state_model.uml
new file mode 100644
index 0000000000000000000000000000000000000000..29cd4077c456931c074e17711f1c1762b7089056
--- /dev/null
+++ b/docs/source/api/subarray/subarray_obs_state_model.uml
@@ -0,0 +1,40 @@
+EMPTY: The subarray has no resources
+RESOURCING: The subarray is performing a resourcing operation
+IDLE: The subarray is unconfigured
+CONFIGURING: The subarray is performing a configuring operation
+READY: The subarray is configured
+SCANNING: The subarray is scanning
+ABORTING: The subarray is aborting
+ABORTED: The subarray has aborted
+RESETTING: The subarray is resetting to IDLE\nfrom FAULT or ABORTED state
+RESTARTING: The subarray is restarting to EMPTY\nfrom FAULT or ABORTED state
+FAULT: The subarray has faulted
+
+EMPTY -down-> RESOURCING: assign_invoked
+RESOURCING -up-> EMPTY: assign_completed
+RESOURCING -up-> EMPTY: release_completed
+RESOURCING -down-> IDLE: assign_completed
+RESOURCING -down-> IDLE: release_completed
+IDLE -up-> RESOURCING: assign_invoked
+IDLE -up-> RESOURCING: release_invoked
+IDLE -down-> CONFIGURING: configure_invoked
+IDLE -left-> ABORTING: abort_invoked
+CONFIGURING -up-> IDLE: configure_completed
+CONFIGURING -left-> ABORTING: abort_invoked
+CONFIGURING -down-> READY: configure_completed
+CONFIGURING -left-> ABORTING: abort_invoked
+READY -up-> CONFIGURING: configure_invoked
+READY -up-> IDLE: component_unconfigured
+READY -down-> SCANNING: component_scanning
+READY -left-> ABORTING: abort_invoked
+SCANNING -up-> READY: component_not_scanning
+SCANNING -left-> ABORTING: abort_invoked
+ABORTING -up-> ABORTED: abort_completed
+ABORTED -up-> RESETTING: obsreset_invoked
+ABORTED -up-> RESTARTING: restart_invoked
+RESETTING -down-> ABORTING: abort_invoked
+RESETTING -right-> IDLE: obsreset_completed
+RESTARTING -right-> EMPTY: restart_completed
+[*] -up-> FAULT: component_obsfault\n(from any state)
+FAULT -up-> RESETTING: obsreset_invoked
+FAULT -up-> RESTARTING: restart_invoked
diff --git a/docs/source/api/tel_state_device.rst b/docs/source/api/tel_state_device.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b32cb68e3b4d1b8d60d114779048e37a6c4acc27
--- /dev/null
+++ b/docs/source/api/tel_state_device.rst
@@ -0,0 +1,6 @@
+================
+Tel State Device
+================
+
+.. automodule:: ska_tango_base.tel_state_device
+   :members:
diff --git a/docs/source/api/utils.rst b/docs/source/api/utils.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c145bd88c9a04dfa5d874bac387d7092ee931600
--- /dev/null
+++ b/docs/source/api/utils.rst
@@ -0,0 +1,6 @@
+=====
+Utils
+=====
+
+.. automodule:: ska_tango_base.utils
+   :members:
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8871ddcafd1f0f288cb9a54914a3196a9002214c..77f763065c72d3e0bf1f30088f17061d9d7c0d07 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -99,9 +99,13 @@ extensions = [
     'sphinx.ext.doctest',
     'sphinx.ext.todo',
     'sphinx.ext.coverage',
+    "sphinx.ext.inheritance_diagram",
     'sphinx.ext.viewcode',
     'sphinxcontrib.plantuml'
 ]
+autoclass_content = "class"
+plantuml_syntax_error_image = True
+
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
diff --git a/docs/source/draw_state_machines.py b/docs/source/draw_state_machines.py
deleted file mode 100644
index 16c66978a1bbb169424f101c6a3004b4663d08b4..0000000000000000000000000000000000000000
--- a/docs/source/draw_state_machines.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""
-This module draws diagrams of the state machines.
-
-Usage:
-    ~/ska-src/ska-tango-base$ docker run --rm -ti -v $PWD:/app continuumio/miniconda3 bash
-
-    (base) root@293f3b699c9b:#
-    $ conda install --yes pygraphviz
-    $ pip install transitions
-    $ apt-get update && apt-get install gsfonts
-    $ cd /app/docs/source
-    $ python draw_state_machines.py
-
-(Or see the top level Makefile)
-
-"""
-import importlib
-import sys
-
-from unittest import mock
-from transitions.extensions import GraphMachine
-
-# local import, so we can run this without installing the whole package
-sys.path.append("../../src/ska_tango_base")
-import state_machine
-import csp_subelement_state_machine
-
-
-def patch_in_graph_machine():
-    with mock.patch("transitions.extensions.LockedMachine", GraphMachine):
-        mod = sys.modules["state_machine"]
-        importlib.reload(mod)
-        mod = sys.modules["csp_subelement_state_machine"]
-        importlib.reload(mod)
-
-
-def draw(machine_class):
-    machine_name = machine_class.__name__
-    graphing_options = {
-        "title": f"\n{machine_name}",
-        "show_conditions": True,
-        "show_state_attributes": True,
-        "show_auto_transitions": False,
-    }
-    machine = machine_class(None, **graphing_options)
-    machine.get_graph().draw(f"images/{machine_name}_autogenerated.png", prog='dot')
-
-
-if __name__ == "__main__":
-    patch_in_graph_machine()
-    draw(state_machine.OperationStateMachine)
-    draw(state_machine.AdminModeStateMachine)
-    draw(state_machine.ObservationStateMachine)
-    draw(csp_subelement_state_machine.CspSubElementObsDeviceStateMachine)
diff --git a/docs/source/guide/component_managers.rst b/docs/source/guide/component_managers.rst
new file mode 100644
index 0000000000000000000000000000000000000000..cd182d01b88a3aa1c1abf57fb57b0957b3905ed3
--- /dev/null
+++ b/docs/source/guide/component_managers.rst
@@ -0,0 +1,131 @@
+=================================
+Components and component managers
+=================================
+
+A fundamental assumption of this package is that each Tango device
+exists to provide monitoring and control of some *component* of a SKA
+telescope.
+
+A *component* could be (for example):
+
+* Hardware such as an antenna, dish, atomic clock, TPM, switch, etc
+
+* An external service such as a database or cluster workload
+  manager
+
+* A software process or thread launched by the Tango device.
+
+* In a hierarchical system, a group of subservient Tango devices.
+
+By analogy, if the *component* is a television, the Tango device would
+be the remote control for that television.
+
+Tango devices and their components
+----------------------------------
+Note the distinction between a component and the Tango device that is
+responsible for monitoring and controlling that component.
+
+A component might be hardware equipment installed on site, such as a
+dish or an antenna. The Tango device that monitors that component is a
+software object, in a process running on a server, probably located in a
+server room some distance away. Thus the Tango device and its component
+are largely independent of each other:
+
+* A Tango device may be running normally when its component is in a 
+  fault state, or turned off, or even not fitted. Device states like
+  ``OFF`` and ``FAULT`` represent the state of the monitored component.
+  A Tango device that reports ``OFF`` state is running normally, and
+  reporting that its component is turned off. A Tango device that
+  reports ``FAULT`` state is running normally, and reporting that its
+  component is in a fault state.
+
+* When a Tango device itself experiences a fault (for example, its
+  server crashes), this is not expected to affect the component. The
+  component continues to run; the only impact is it can no longer be
+  monitored or controlled.
+  
+  By analogy: when the batteries in your TV remote control go flat, the
+  TV continues to run.
+
+* We should not assume that a component's state is governed solely by
+  its Tango device. On the contrary, components are influenced by a
+  wide range of factors. For example, the following are ways in which a
+  component might be switched off:
+
+  * Its Tango device switches it off via its software interface;
+
+  * Some other software entity switches it off via its software
+    interface;
+
+  * The hardware switches itself off, or its firmware switches it off,
+    because it detected a critical fault.
+
+  * The equipment's power button is pressed;
+
+  * An upstream power supply device denies it power.
+
+  A Tango device therefore must not treat its component as under its
+  sole control. For example, having turned its component on, it must not
+  assume that the component will remain on. Rather, it must continually
+  *monitor* its component, and update its state to reflect changes in
+  component state.
+  
+Component monitoring
+--------------------
+Component *monitoring* is the main mechanism by which a Tango device
+maintains and updates its state:
+  
+* A Tango device should not make assumptions about component state after
+  issuing a command. For example, after successfully telling its
+  component to turn on, a Tango device should not assume that the
+  component is on, and transition immediately to ON state. Rather, it
+  should wait for its monitoring of the component to provide
+  confirmation that the component is on; only then should it transition
+  to ON state. It follows that a Tango device's ``On()`` command might
+  complete successfully, yet the device's ``state()`` might not report
+  ``ON`` state immediately, or for some seconds, or indeed at all.
+
+* A Tango device also should not make assumptions about component state
+  when the Tango device is *initialising*. For example, in a normal
+  controlled startup of a telescope, an initialising Tango device might
+  expect to find its component switched off, and to be itself
+  responsible for switching the component on at the proper time.
+  However, this is not the only circumstance in which a Tango device
+  might initialise; the Tango device would also have to initialise
+  following a reboot of the server on which it runs. In such a case, the
+  component might already be switched on. Thus, at initialisation, a
+  Tango device should merely launch the component monitoring that will
+  allows the device to detect the state of the component.
+
+Component managers
+------------------
+A Tango device's responsibility to monitor and control its component is
+largely separate from its interface to the Tango subsystem. Therefore,
+devices in this package implement component monitoring and control in a
+separate *component manager*.
+
+A component manager is responsible for:
+
+  * establishing and maintaining communication with the component. For
+    example:
+
+    * If the component interface is via a connection-oriented protocol
+      (such as TCP/IP), then the component manager must establish and
+      maintain a *connection* to the component;
+
+    * If the component is able to publish updates, then the component
+      manager would need to subscribe to those updates;
+  
+    * If the component cannot publish updates, but can only respond to
+      requests, then the component manager would need to initiate
+      polling of the component.
+
+  * implementing monitoring of the component so that changes in
+    component state trigger callbacks that report those changes up to
+    the Tango device;
+    
+  * implementing commands such as ``off()``, ``on()``, etc., so that
+    they actually tell the component to turn off, turn on, etc.
+
+.. note:: It is highly recommended to implement your component manager,
+   and thoroughly test it, *before* embedding it in a Tango device.
diff --git a/docs/source/guide/getting_started.rst b/docs/source/guide/getting_started.rst
new file mode 100644
index 0000000000000000000000000000000000000000..0b67fedfefddd4108b1a15f5baf9a88ff0b5cba6
--- /dev/null
+++ b/docs/source/guide/getting_started.rst
@@ -0,0 +1,317 @@
+===============
+Getting started
+===============
+This page will guide you through the steps to writing a SKA Tango device
+based on the ``ska-tango-base`` package.
+
+Prerequisites
+-------------
+It is assumed here that you have a subproject repository, and have `set
+up your development environment`_. The ``ska-tango-base`` package can be
+installed from the EngageSKA Nexus repository:
+
+.. code-block:: shell-session
+
+   me@local:~$ python3 -m pip install --extra-index-url https://nexus.engageska-portugal.pt/repository/pypi/simple ska-tango-base
+
+Basic steps
+-----------
+The recommended basic steps to writing a SKA Tango device based on the
+``ska-tango-base`` package are:
+
+1. Write a component manager.
+
+2. Implement command class objects.
+
+3. Write your Tango device.
+
+Detailed steps
+--------------
+
+Write a component manager
+^^^^^^^^^^^^^^^^^^^^^^^^^
+A fundamental assumption of this package is that each Tango device
+exists to provide monitoring and control of some *component* of a SKA
+telescope. That *component* could be some hardware, a software service
+or process, or even a group of subservient Tango devices.
+
+A *component manager* provides for monitoring and control of a
+component. It is *highly recommended* to implement and thoroughly test
+your component manager *before* embedding it in a Tango device.
+
+For more information on components and component managers, see
+:doc:`component_managers`.
+
+Writing a component manager involves the following steps.
+
+1. **Choose a subclass for your component manager.** There are several
+   component manager base classes, each associated with a device class.
+   For example,
+   
+   * If your Tango device will inherit from ``SKABaseDevice``, then you
+     will probably want to base your component manager on the
+     ``BaseComponentManager`` class.
+
+   * If your Tango device is a subarray, then you will want to base your
+     component manager on ``SubarrayComponentManager``.
+
+   .. inheritance-diagram::
+      ska_tango_base.base.component_manager
+      ska_tango_base.subarray.component_manager
+      ska_tango_base.csp.obs.component_manager
+      ska_tango_base.csp.subarray.component_manager
+      :parts: 1
+  
+
+   These component managers are abstract. They specify an interface, but
+   leave it up to you to implement the functionality. For example,
+   ``BaseComponentManager``'s ``on()`` command looks like this:
+
+   .. code-block:: py
+
+     def on(self):
+       raise NotImplementedError("BaseComponentManager is abstract.")
+   
+   Your component manager will inherit these methods, and override them
+   with actual implementations.
+
+   .. note:: In addition to these abstract classes, there are also
+      reference implementations of concrete subclasses. For example, in
+      addition to an abstract ``BaseComponentManager``, there is also a
+      concrete ``ReferenceBaseComponentManager``. These reference
+      implementations are provided for explanatory purposes: they
+      illustrate how a concrete component manager might be implemented.
+      You are encouraged to review the reference implementations, and
+      adapt them to your own needs; but it is not recommended to
+      subclass from them.
+
+2. **Establish communication with your component.** How you do this will
+   depend on the capabilities and interface of your component. for
+   example:
+
+   * If the component interface is via a connection-oriented protocol
+     (such as TCP/IP), then the component manager must establish and
+     maintain a *connection* to the component;
+
+   * If the component is able to publish updates, then the component
+     manager would need to subscribe to those updates;
+
+   * If the component cannot publish updates, but can only respond to
+     requests, then the component manager would need to initiate
+     polling of the component.
+
+4. **Implement component monitoring.** Whenever your component changes
+   its state, your component manager needs to become reliably aware of
+   that change within a reasonable timeframe, so that it can pass this
+   on to the Tango device.
+   
+   The abstract component managers provided already contain some helper
+   methods to trigger device callbacks. For example,
+   ``BaseComponentManager`` provides a ``component_fault`` method that
+   lets the device know that the component has experienced a fault. You
+   need to implement component monitoring so that, if the component
+   experiences a fault, this is detected, and results in the
+   ``component_fault`` helper method being called.
+
+   For component-specific functionality, you will need to implement the
+   corresponding helper methods. For example, if your component reports
+   its temperature, then your component manager will need to
+
+   1. Implement a mechanism by which it can let its Tango device know
+      that the component temperature has changed, such as a callback;
+
+   2. Implement monitoring so that this mechanism is triggered whenever
+      a change in component temperature is detected.
+
+5. **Implement component control.** Methods to control the component
+   must be implemented; for example the component manager's ``on()``
+   method must be implemented to actually tell the component to turn on.
+
+   Note that component *control* and component *monitoring* are
+   decoupled from each other. So, for example, a component manager's
+   ``on()`` method should not directly call the callback that tells the
+   device that the component is now on. Rather, the command should
+   return without calling the callback, and leave it to the *monitoring*
+   to detect when the component has changed states.
+   
+   Consider, for example, a component that takes ten seconds to power
+   up:
+   
+   1. The ``on()`` command should be implemented to tell the component
+      to power up. If the component accepts this command without
+      complaint, then the ``on()`` command should return success. The
+      component manager should not, however, assume that the component
+      is now on.
+   2. After ten seconds, the component has powered up, and the component
+      manager's monitoring detects that the component is on. Only then
+      should the callback be called to let the device know that the
+      component has changed state, resulting in a change of device state
+      to ``ON``.
+
+.. note:: A component manager may maintain additional state, and support
+   additional commands, that do not map to its component. That is, a
+   call to a component manager needs not always result in a call to the
+   underlying component. For example, a subarray's component manager may
+   implement its ``assign_resources`` method simply to maintain a record
+   (within the component manager itself) of what resources it has, so
+   that it can validate arguments to other methods (for example, check
+   that arguments to its ``configure`` method do not require access to
+   resources that have not been assigned to it). In this case, the call
+   to the component manager's ``assign_resources`` method would not
+   result in interaction with the component; indeed, the component may
+   not even possess the concepts of *resources* and *resource
+   assignment*.
+
+Implement command class objects
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Tango device command functionality is implemented in command *classes*
+rather than methods. This allows for:
+   
+* functionality common to many classes to be abstracted out and
+  implemented once for all. For example, there are many commands
+  associated with transitional states (*e.g.* ``Configure()`` command
+  and ``CONFIGURING`` state, ``Scan()`` command and ``SCANNING`` state,
+  *etc.*). Command classes allow us to implement this association once
+  for all, and to protect that implementation from accidental overriding
+  by command subclasses.
+* testing of commands independently of Tango. For example, a Tango
+  device's ``On()`` command might only need to interact with the
+  device's component manager and its operational state model. As such,
+  in order to test the correct implementation of that command, we only
+  need a component manager and an operational state model. Thus, we can
+  test the command without actually instantiating the Tango device.
+
+Writing a command class involves the following steps.
+
+1. **Do you really need to implement the command?** If the command to be
+   implemented is part of the Tango device you will inherit from,
+   perhaps the current implementation is exactly what you need.
+
+   For example, the ``SKABaseDevice`` class's implementation of the
+   ``On()`` command simply calls its component manager's ``on()``
+   method. Maybe you don't need to change that; you've implemented your
+   component manager's ``on()`` method, and that's all there is to do.
+
+2. **Choose a command class to subclass.**
+
+   * If the command to be implemented is part of the device you will
+     inherit from (but you still need to override it), then you would
+     generally subclass the base device's command class. For example, if
+     if you need to override ``SKABaseDevice``'s ``Standby`` command,
+     then you would subclass ``SKABaseDevice.StandbyCommand``.
+
+   * If the command is a new command, not present in the base device
+     class, then you will want to inherit from one or more command
+     classes in the :py:mod:`ska_tango_base.commands` module. 
+
+     .. inheritance-diagram::
+        ska_tango_base.commands.BaseCommand
+        ska_tango_base.commands.StateModelCommand
+        ska_tango_base.commands.ResponseCommand
+        ska_tango_base.commands.CompletionCommand
+        ska_tango_base.commands.ObservationCommand
+        :parts: 1
+
+3. **Implement class methods.**
+   
+   * In many cases, you only need to implement the ``do()`` method.
+
+   * To constrain when the command is allowed to be invoked, override
+     the ``is_allowed()`` method.
+
+
+Write your Tango device
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Writing the Tango device involves the following steps:
+
+1. **Select a device class to subclass.**
+
+   .. inheritance-diagram::
+      ska_tango_base.SKAAlarmHandler
+      ska_tango_base.SKACapability
+      ska_tango_base.SKALogger
+      ska_tango_base.SKAMaster
+      ska_tango_base.SKATelState
+      ska_tango_base.base.SKABaseDevice
+      ska_tango_base.obs.SKAObsDevice
+      ska_tango_base.subarray.SKASubarray
+      ska_tango_base.csp.CspSubElementMaster
+      ska_tango_base.csp.CspSubElementObsDevice
+      ska_tango_base.csp.CspSubElementSubarray
+      :top-classes: ska_tango_base.base.SKABaseDevice
+      :parts: 1
+
+2. **Register your component manager.** This is done by overriding the
+   ``create_component_manager`` class to return your component manager
+   object:
+
+   .. code-block:: py
+
+     def create_component_manager(self):
+         return AntennaComponentManager(
+             self.op_state_model, logger=self.logger
+         )
+
+3. **Implement commands.** You've already written the command classes.
+   There is some boilerplate to ensure that the Tango command methods
+   invoke the command classes:
+
+   1. Registration occurs in the ``init_command_objects`` method, using
+      calls to the ``register_command_object`` helper method. Implement
+      the ``init_command_objects`` method:
+
+      .. code-block:: py
+
+         def init_command_objects(self):
+             super().init_command_objects()
+
+             self.register_command_object(
+                 "DoStuff", self.DoStuffCommand(self.component_manager, self.logger)
+             )
+             self.register_command_object(
+                 "DoOtherStuff", self.DoOtherStuffCommand(
+                     self.component_manager, self.logger
+                 )
+             )
+
+   2. Any new commands need to be implemented as:
+
+      .. code-block:: py
+
+         @command(dtype_in=..., dtype_out=...)
+         def DoStuff(self, argin):
+             command = self.get_command_object("DoStuff")
+             return command(argin)
+
+      or, if the command does not take an argument:
+
+      .. code-block:: py
+
+         @command(dtype_out=...)
+         def DoStuff(self):
+             command = self.get_command_object("DoStuff")
+             return command()
+
+      Note that these two examples deliberately push all SKA business
+      logic down to the command class (at least) or even the component
+      manager. It is highly recommended not to include SKA business
+      logic in Tango devices. However, Tango-specific functionality can
+      and should be implemented directly into the command method. For
+      example, many SKA commands accept a JSON string as argument, as a
+      workaround for the fact that Tango commands cannot accept more
+      than one argument. Since this use of JSON is closely associated
+      with Tango, we might choose to unpack our JSON strings in the
+      command method itself, thus leaving our command objects free of
+      JSON:
+
+      .. code-block:: py
+
+         @command(dtype_in=..., dtype_out=...)
+         def DoStuff(self, argin):
+             args = json.loads(argin)
+             command = self.get_command_object("DoStuff")
+             return command(args)
+
+
+.. _set up your development environment: https://developer.skatelescope.org/en/latest/tools/tango-devenv-setup.html
\ No newline at end of file
diff --git a/docs/source/guide/index.rst b/docs/source/guide/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2d0b701bba26080cce22b54ff11659da1fbacccc
--- /dev/null
+++ b/docs/source/guide/index.rst
@@ -0,0 +1,8 @@
+===============
+Developer Guide
+===============
+
+.. toctree::
+
+  Getting started<getting_started>
+  Components and component managers<component_managers>
diff --git a/docs/source/images/AdminModeStateMachine_autogenerated.png b/docs/source/images/AdminModeStateMachine_autogenerated.png
deleted file mode 100644
index 6c761f570cdbdeb0fa41b6a157ffea9736bf19ed..0000000000000000000000000000000000000000
Binary files a/docs/source/images/AdminModeStateMachine_autogenerated.png and /dev/null differ
diff --git a/docs/source/images/CspSubElementObsDeviceStateMachine_autogenerated.png b/docs/source/images/CspSubElementObsDeviceStateMachine_autogenerated.png
deleted file mode 100644
index 41acb9e01422ecb251c4d9801055161266853bd6..0000000000000000000000000000000000000000
Binary files a/docs/source/images/CspSubElementObsDeviceStateMachine_autogenerated.png and /dev/null differ
diff --git a/docs/source/images/ObservationStateMachine_autogenerated.png b/docs/source/images/ObservationStateMachine_autogenerated.png
deleted file mode 100644
index 2299fbbe566219224e8f7b7179828c03298d6115..0000000000000000000000000000000000000000
Binary files a/docs/source/images/ObservationStateMachine_autogenerated.png and /dev/null differ
diff --git a/docs/source/images/OperationStateMachine_autogenerated.png b/docs/source/images/OperationStateMachine_autogenerated.png
deleted file mode 100644
index a9845db4166b4727af342d486079e2453676e091..0000000000000000000000000000000000000000
Binary files a/docs/source/images/OperationStateMachine_autogenerated.png and /dev/null differ
diff --git a/docs/source/images/OperationStateMachine_coupled.png b/docs/source/images/OperationStateMachine_coupled.png
deleted file mode 100644
index c2e4e248213dee574cf47f484873be631dc77734..0000000000000000000000000000000000000000
Binary files a/docs/source/images/OperationStateMachine_coupled.png and /dev/null differ
diff --git a/docs/source/images/OperationStateMachine_decoupled.png b/docs/source/images/OperationStateMachine_decoupled.png
deleted file mode 100644
index 057aef047390af60da94e7d574ad6c876b8b36cd..0000000000000000000000000000000000000000
Binary files a/docs/source/images/OperationStateMachine_decoupled.png and /dev/null differ
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 036121da3642aec245cfbae9256e96b1f98f3044..202a3d51a0ce26837fef05f8657f5b9b826f8007 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -1,40 +1,19 @@
-.. SKA Tango Base documentation master file, created by
-   sphinx-quickstart on Fri Jan 11 10:03:42 2019.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
 Welcome to SKA Tango Base documentation!
 ========================================
 
-.. toctree::
-  :caption: Standard Devices
-  :maxdepth: 2
+.. This package provides base devices and functionality for the Tango
+.. devices that provide local monitoring and control (LMC) of SKA telescope
+.. components
 
-   Base Device<SKABaseDevice>
-   Alarm Handler<SKAAlarmHandler>
-   Logger<SKALogger>
-   Master<SKAMaster>
-   Tel State<SKATelState>
+.. automodule:: ska_tango_base
 
-   Obs Device<SKAObsDevice>
-   Capability<SKACapability>
-   Subarray<SKASubarray>
 
 .. toctree::
-  :caption: CSP Devices
-  :maxdepth: 2
+  :maxdepth: 1
 
-   CSP Sub-element Master<CspSubElementMaster>
-   CSP Sub-element Obs Device<CspSubElementObsDevice>
-   CSP Sub-element Subarray<CspSubElementSubarray>
-
-.. toctree::
-  :caption: Control Model
-  :maxdepth: 2
+  Developer Guide<guide/index>
+  API<api/index>
 
-   Control Model<Control_Model>
-   Commands<Commands>
-   State Machine<State_Machine>
 
 Indices and tables
 ==================
diff --git a/docs/source/scripts/draw_state_machines.py b/docs/source/scripts/draw_state_machines.py
new file mode 100644
index 0000000000000000000000000000000000000000..6142508c4eb9f4c5c5c5bfcc36737cc1a091e642
--- /dev/null
+++ b/docs/source/scripts/draw_state_machines.py
@@ -0,0 +1,68 @@
+"""
+This module draws diagrams of the state machines.
+
+Usage:
+    ~/ska-src/ska-tango-base$ docker run --rm -ti -v $PWD:/app continuumio/miniconda3 bash
+
+    (base) root@293f3b699c9b:#
+    $ conda install --yes pygraphviz
+    $ pip install transitions
+    $ apt-get update && apt-get install gsfonts
+    $ cd /app/docs/source
+    $ python draw_state_machines.py
+
+(Or see the top level Makefile)
+
+"""
+import importlib
+import sys
+
+from unittest import mock
+from transitions.extensions import GraphMachine
+
+import ska_tango_base
+
+
+def patch_in_graph_machine():
+    module_names = [
+        "ska_tango_base.base.op_state_model",
+        "ska_tango_base.base.admin_mode_model",
+        "ska_tango_base.subarray.subarray_obs_state_model",
+        "ska_tango_base.csp.csp_subelement_obs_state_model",
+    ]
+    with mock.patch("transitions.extensions.LockedMachine", GraphMachine):
+        for module_name in module_names:
+            mod = sys.modules[module_name]
+            importlib.reload(mod)
+
+
+def draw(machine_class, target_filename):
+    machine_name = machine_class.__name__
+    graphing_options = {
+        "title": f"\n{machine_name}",
+        "show_conditions": True,
+        "show_state_attributes": True,
+        "show_auto_transitions": False,
+    }
+    machine = machine_class(None, **graphing_options)
+    machine.get_graph().draw(target_filename, prog='dot')
+
+
+if __name__ == "__main__":
+    patch_in_graph_machine()
+    draw(
+        ska_tango_base.base.op_state_model._OpStateMachine,
+        "api/base/_OpStateMachine_autogenerated.png",
+    )
+    draw(
+        ska_tango_base.base.admin_mode_model._AdminModeMachine,
+        "api/base/_AdminModeMachine_autogenerated.png",
+    )
+    draw(
+        ska_tango_base.subarray.subarray_obs_state_model._SubarrayObsStateMachine,
+        "api/subarray/_SubarrayObsStateMachine_autogenerated.png",
+    )
+    draw(
+        ska_tango_base.csp.csp_subelement_obs_state_model._CspSubElementObsStateMachine,
+        "api/csp/_CspSubElementObsStateMachine_autogenerated.png",
+    )
diff --git a/pogo/CspSubElementMaster.xmi b/pogo/CspSubElementMaster.xmi
index c6dfe00a341fa364b2ce81eee038bdba4d885539..ad01e7eea323228cfd805e30a0652767a4fe4cc4 100644
--- a/pogo/CspSubElementMaster.xmi
+++ b/pogo/CspSubElementMaster.xmi
@@ -21,7 +21,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -66,7 +66,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -129,7 +129,7 @@
       </argout>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </commands>
-    <commands name="ReInitDevices" description="The exact functionality may vary for different devices &#xA;and sub-systems, each TANGO Device/Server&#xA;should define what does ReinitDevices means.&#xA;Ex:&#xA;ReInitDevices FPGA &#x2192; reset&#xA;ReInitDevices Master &#x2192; Restart&#xA;ReInitDevices Leaf PC &#x2192; reboot" execMethod="re_init_devices" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
+    <commands name="ReInitDevices" description="The exact functionality may vary for different devices &#xA;and sub-systems, each Tango Device/Server&#xA;should define what does ReinitDevices means.&#xA;Ex:&#xA;ReInitDevices FPGA &#x2192; reset&#xA;ReInitDevices Master &#x2192; Restart&#xA;ReInitDevices Leaf PC &#x2192; reboot" execMethod="re_init_devices" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
       <argin description="List of devices to re-initialize">
         <type xsi:type="pogoDsl:StringArrayType"/>
       </argin>
@@ -206,7 +206,7 @@
       <changeEvent fire="false" libCheckCriteria="false"/>
       <archiveEvent fire="false" libCheckCriteria="false"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/CspSubElementObsDevice.xmi b/pogo/CspSubElementObsDevice.xmi
index 217ebab8475db969d99302381d65493270ff8267..e00bcbb9f962d0fe319259d50eb520dc5a7d0887 100644
--- a/pogo/CspSubElementObsDevice.xmi
+++ b/pogo/CspSubElementObsDevice.xmi
@@ -21,7 +21,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -48,7 +48,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -111,7 +111,7 @@
       </argout>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </commands>
-    <commands name="ObsReset" description="Reset the observing device from a FAULT/ABORTED state to IDLE." execMethod="obs_reset" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
+    <commands name="ObsReset" description="Reset the observing device from a FAULT/ABORTED state to IDLE." execMethod="obsreset" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -185,7 +185,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/CspSubElementSubarray.xmi b/pogo/CspSubElementSubarray.xmi
index 6b6b9b8233f4c6936734dda50c207300ee2f978b..aef57c91833c7c850e05f6d9d05ee550b860f310 100644
--- a/pogo/CspSubElementSubarray.xmi
+++ b/pogo/CspSubElementSubarray.xmi
@@ -20,7 +20,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -51,7 +51,7 @@
       </argout>
       <status abstract="false" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -141,7 +141,7 @@
       </argout>
       <status abstract="false" inherited="true" concrete="true"/>
     </commands>
-    <commands name="ObsReset" description="Reset observation state machine to its default state" execMethod="obs_reset" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="ObsReset" description="Reset observation state machine to its default state" execMethod="obsreset" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -220,7 +220,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="healthState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKAAlarmHandler.xmi b/pogo/SKAAlarmHandler.xmi
index d66d34d008ab6022e95a9da57cd966b5d531e002..2a6b5b834bf1268f259d3ce7311ebcb0c7413706 100644
--- a/pogo/SKAAlarmHandler.xmi
+++ b/pogo/SKAAlarmHandler.xmi
@@ -19,7 +19,7 @@
       <status abstract="false" inherited="true" concrete="true"/>
       <DefaultPropValue>4</DefaultPropValue>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -113,7 +113,7 @@
       </argout>
       <status abstract="false" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -203,7 +203,7 @@
       <changeEvent fire="false" libCheckCriteria="false"/>
       <archiveEvent fire="false" libCheckCriteria="false"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKABaseDevice.xmi b/pogo/SKABaseDevice.xmi
index dec232f5082926c656a64ab642c186dfe07d91ff..4d5e1f2787a11ae53216d9feada346389a25f82f 100644
--- a/pogo/SKABaseDevice.xmi
+++ b/pogo/SKABaseDevice.xmi
@@ -10,7 +10,7 @@
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
       <DefaultPropValue>4</DefaultPropValue>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </deviceProperties>
@@ -41,7 +41,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true" concreteHere="false"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0" isDynamic="false">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -121,7 +121,7 @@
       <dataType xsi:type="pogoDsl:EnumType"/>
       <dataReadyEvent fire="false" libCheckCriteria="true"/>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
       <enumLabels>REMOTE</enumLabels>
       <enumLabels>LOCAL</enumLabels>
     </attributes>
diff --git a/pogo/SKACapability.xmi b/pogo/SKACapability.xmi
index 824d079a622c53f84d072c48a429ef86ee566eae..0273b3bc7f0a5cd0395f5c55f9d2313af07a814d 100644
--- a/pogo/SKACapability.xmi
+++ b/pogo/SKACapability.xmi
@@ -20,7 +20,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -54,7 +54,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -154,7 +154,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKALogger.xmi b/pogo/SKALogger.xmi
index b1531989dabd7b95b1accbe272f2b10c9ff5b63a..e4ca24ea7a0ef5148d9220adaa469d0346e07590 100644
--- a/pogo/SKALogger.xmi
+++ b/pogo/SKALogger.xmi
@@ -11,7 +11,7 @@
       <status abstract="false" inherited="true" concrete="true"/>
       <DefaultPropValue>4</DefaultPropValue>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -51,7 +51,7 @@
       </argout>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -111,7 +111,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKAMaster.xmi b/pogo/SKAMaster.xmi
index 8463630a2e3a6ebea32856698e3296e23632f93c..4d9a8bdcc72bccbfef674fdd1e6f86ad06216beb 100644
--- a/pogo/SKAMaster.xmi
+++ b/pogo/SKAMaster.xmi
@@ -20,7 +20,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -55,7 +55,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -164,7 +164,7 @@
       <changeEvent fire="false" libCheckCriteria="false"/>
       <archiveEvent fire="false" libCheckCriteria="false"/>
       <status abstract="false" inherited="true" concrete="true" concreteHere="false"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKAObsDevice.xmi b/pogo/SKAObsDevice.xmi
index d10090156d372e8032ac19abaf881c70b8a0fb35..1e4a08b052eec4a74762164ec7163ee22aaf8e5f 100644
--- a/pogo/SKAObsDevice.xmi
+++ b/pogo/SKAObsDevice.xmi
@@ -20,7 +20,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -42,7 +42,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -149,7 +149,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true" concreteHere="false"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKASubarray.xmi b/pogo/SKASubarray.xmi
index b5e5e06f3f1daf79a8816028b09d8c4a0cb295b3..b4f607491a71427a0aecdd7f87701aafd9ea2c8d 100644
--- a/pogo/SKASubarray.xmi
+++ b/pogo/SKASubarray.xmi
@@ -19,7 +19,7 @@
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -50,7 +50,7 @@
       </argout>
       <status abstract="false" inherited="false" concrete="true" concreteHere="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -201,7 +201,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="healthState" attType="Scalar" rwType="READ" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/pogo/SKATelState.xmi b/pogo/SKATelState.xmi
index 4c3e3142d043862a8fa27b2ebb57433150eaa47d..5c3716d0bbd631ff69d425646c72776bb6690615 100644
--- a/pogo/SKATelState.xmi
+++ b/pogo/SKATelState.xmi
@@ -15,7 +15,7 @@
       <status abstract="false" inherited="true" concrete="true"/>
       <DefaultPropValue>4</DefaultPropValue>
     </deviceProperties>
-    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A TANGO Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
+    <deviceProperties name="GroupDefinitions" description="Each string in the list is a JSON serialised dict defining the ``group_name``,&#xA;``devices`` and ``subgroups`` in the group.  A Tango Group object is created&#xA;for each item in the list, according to the hierarchy defined.  This provides&#xA;easy access to the managed devices in bulk, or individually.&#xA;&#xA;The general format of the list is as follows, with optional ``devices`` and&#xA;``subgroups`` keys:&#xA;    [ {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ...]},&#xA;      {``group_name``: ``&lt;name>``,&#xA;       ``devices``: [``&lt;dev name>``, ``&lt;dev name>``, ...],&#xA;       ``subgroups`` : [{&lt;nested group>},&#xA;                              {&lt;nested group>}, ...]},&#xA;      ...&#xA;      ]&#xA;&#xA;For example, a hierarchy of racks, servers and switches:&#xA;    [ {``group_name``: ``servers``,&#xA;       ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                       ``elt/server/3``, ``elt/server/4``]},&#xA;      {``group_name``: ``switches``,&#xA;       ``devices``: [``elt/switch/A``, ``elt/switch/B``]},&#xA;      {``group_name``: ``pdus``,&#xA;       ``devices``: [``elt/pdu/rackA``, ``elt/pdu/rackB``]},&#xA;      {``group_name``: ``racks``,&#xA;      ``subgroups``: [&#xA;            {``group_name``: ``rackA``,&#xA;             ``devices``: [``elt/server/1``, ``elt/server/2``,&#xA;                               ``elt/switch/A``, ``elt/pdu/rackA``]},&#xA;            {``group_name``: ``rackB``,&#xA;             ``devices``: [``elt/server/3``, ``elt/server/4``,&#xA;                              ``elt/switch/B``, ``elt/pdu/rackB``],&#xA;             ``subgroups``: []}&#xA;       ]} ]">
       <type xsi:type="pogoDsl:StringVectorType"/>
       <status abstract="false" inherited="true" concrete="true"/>
     </deviceProperties>
@@ -46,7 +46,7 @@
       </argout>
       <status abstract="true" inherited="true" concrete="true"/>
     </commands>
-    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this TANGO Device itself.&#xA;The entities may be TANGO devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
+    <commands name="GetVersionInfo" description="Array of version strings of all entities modelled by this device. &#xA;(One level down only)&#xA;Each string in the array lists the version info for one entity&#xA;managed by this device. &#xA;The first entry is version info for this Tango Device itself.&#xA;The entities may be Tango devices, or hardware LRUs or &#xA;anything else this devices manages/models.&#xA;The intention with this command is that it can provide more &#xA;detailed information than can be captured in the versionId &#xA;and buildState attributes, if necessary.&#xA;In the minimal case the GetVersionInfo will contain only the &#xA;versionId and buildState attributes of the next lower level&#xA;entities." execMethod="get_version_info" displayLevel="OPERATOR" polledPeriod="0">
       <argin description="">
         <type xsi:type="pogoDsl:VoidType"/>
       </argin>
@@ -106,7 +106,7 @@
     <attributes name="controlMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
       <status abstract="false" inherited="true" concrete="true"/>
-      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;TANGO Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
+      <properties description="The control mode of the device. REMOTE, LOCAL&#xA;Tango Device accepts only from a &#x2018;local&#x2019; client and ignores commands and queries received from TM&#xA;or any other &#x2018;remote&#x2019; clients. The Local clients has to release LOCAL control before REMOTE clients&#xA;can take control again." label="" unit="" standardUnit="" displayUnit="" format="" maxValue="" minValue="" maxAlarm="" minAlarm="" maxWarning="" minWarning="" deltaTime="" deltaValue=""/>
     </attributes>
     <attributes name="simulationMode" attType="Scalar" rwType="READ_WRITE" displayLevel="OPERATOR" polledPeriod="0" maxX="" maxY="" memorized="true" allocReadMember="true">
       <dataType xsi:type="pogoDsl:EnumType"/>
diff --git a/scripts/purge_xmi_tree.py b/scripts/purge_xmi_tree.py
index 698f2b9cdf1212dce359bd0774c6acd7fe98b1c0..90c728c7904640e4b2ed3296ae3503f84d51a067 100755
--- a/scripts/purge_xmi_tree.py
+++ b/scripts/purge_xmi_tree.py
@@ -126,7 +126,7 @@ if __name__ == '__main__':
         psr = sim_xmi_parser.XmiParser()
         psr.parse(filepath)
 
-        # Get all the features of the TANGO class
+        # Get all the features of the Tango class
         attr_qualities = psr.get_device_attribute_metadata()
         cmd_qualities = psr.get_device_command_metadata()
         devprop_qualities = psr.get_device_properties_metadata('deviceProperties')
diff --git a/setup.py b/setup.py
index ac43af8fb3b1694c1d773babd6bc92ec8db9b762..6a8425260745ff5adff921c62cca8d3e350f5797 100644
--- a/setup.py
+++ b/setup.py
@@ -46,13 +46,13 @@ setuptools.setup(
     entry_points={
         "console_scripts": [
             "SKAAlarmHandler=ska_tango_base.alarm_handler_device:main",
-            "SKABaseDevice=ska_tango_base.base_device:main",
+            "SKABaseDevice=ska_tango_base.base.base_device:main",
             "SKACapability=ska_tango_base.capability_device:main",
             "SKAExampleDevice=ska_tango_base.example_device:main",
             "SKALogger=ska_tango_base.logger_device:main",
             "SKAMaster=ska_tango_base.master_device:main",
-            "SKAObsDevice=ska_tango_base.obs_device:main",
-            "SKASubarray=ska_tango_base.subarray_device:main",
+            "SKAObsDevice=ska_tango_base.obs.obs_device:main",
+            "SKASubarray=ska_tango_base.subarray.subarray_device:main",
             "SKATelState=ska_tango_base.tel_state_device:main",
             "CspSubelementMaster=ska_tango_base.csp_subelement_master:main",
             "CspSubelementObsDevice=ska_tango_base.csp_subelement_obsdevice:main",
diff --git a/src/ska_tango_base/__init__.py b/src/ska_tango_base/__init__.py
index f0d311b608b95dfaa22d38851086d7e87646b513..58b441bbf97cc358dc1d5cc7663faadcfe42a3a5 100644
--- a/src/ska_tango_base/__init__.py
+++ b/src/ska_tango_base/__init__.py
@@ -1,47 +1,44 @@
 __all__ = (
+    # subpackages
+    "base",
+    "csp",
+    "obs",
+    "subarray",
+    # modules
     "commands",
     "control_model",
-    "state_machine",
+    "faults",
+    "release",
+    "utils",
+    # direct imports
     "SKAAlarmHandler",
     "SKABaseDevice",
-    "DeviceStateModel",
     "SKACapability",
     "SKALogger",
     "SKAMaster",
     "SKAObsDevice",
     "SKASubarray",
-    "SKASubarrayStateModel",
-    "SKASubarrayResourceManager",
     "SKATelState",
-    "ObsDeviceStateModel",
-    "csp_subelement_state_machine",
     "CspSubElementMaster",
     "CspSubElementObsDevice",
-    "CspSubElementObsDeviceStateModel",
     "CspSubElementSubarray",
 )
 
 # Note: order of imports is important - start with lowest in the hierarchy
 
 # SKABaseDevice, and then classes that inherit from it
-from .base_device import SKABaseDevice, DeviceStateModel
+from .base import SKABaseDevice
 from .alarm_handler_device import SKAAlarmHandler
 from .logger_device import SKALogger
 from .master_device import SKAMaster
 from .tel_state_device import SKATelState
 
 # SKAObsDevice, and then classes that inherit from it
-from .obs_device import SKAObsDevice, ObsDeviceStateModel
+from .obs import SKAObsDevice
 from .capability_device import SKACapability
-from .subarray_device import (
-    SKASubarray,
-    SKASubarrayStateModel,
-    SKASubarrayResourceManager,
-)
+from .subarray import SKASubarray
+
 # CspSubElement classes
-from .csp_subelement_master import CspSubElementMaster
-from .csp_subelement_subarray import CspSubElementSubarray
-from .csp_subelement_obsdevice import (
-        CspSubElementObsDevice,
-        CspSubElementObsDeviceStateModel,
-)
+from .csp import CspSubElementMaster
+from .csp import CspSubElementSubarray
+from .csp import CspSubElementObsDevice
diff --git a/src/ska_tango_base/alarm_handler_device.py b/src/ska_tango_base/alarm_handler_device.py
index d1e74161b064a5dd21c2423eab94b85aab6206b7..22bb34ce37b6836a28a39e5c81dd743b02f2f5a0 100644
--- a/src/ska_tango_base/alarm_handler_device.py
+++ b/src/ska_tango_base/alarm_handler_device.py
@@ -6,10 +6,10 @@
 #
 """
 This module implements SKAAlarmHandler, a generic base device for Alarms
-for SKA. It exposes SKA alarms and SKA alerts as TANGO attributes. SKA
+for SKA. It exposes SKA alarms and SKA alerts as Tango attributes. SKA
 Alarms and SKA/Element Alerts are rules-based configurable conditions
 that can be defined over multiple attribute values and quality factors,
-and are separate from the "built-in" TANGO attribute alarms.
+and are separate from the "built-in" Tango attribute alarms.
 """
 # PROTECTED REGION ID(SKAAlarmHandler.additionnal_import) ENABLED START #
 # Tango imports
@@ -103,23 +103,23 @@ class SKAAlarmHandler(SKABaseDevice):
         super().init_command_objects()
         self.register_command_object(
             "GetAlarmRule",
-            self.GetAlarmRuleCommand(self, self.state_model, self.logger)
+            self.GetAlarmRuleCommand(self, self.op_state_model, self.logger)
         )
         self.register_command_object(
             "GetAlarmData",
-            self.GetAlarmDataCommand(self, self.state_model, self.logger)
+            self.GetAlarmDataCommand(self, self.op_state_model, self.logger)
         )
         self.register_command_object(
             "GetAlarmAdditionalInfo",
-            self.GetAlarmAdditionalInfoCommand(self, self.state_model, self.logger)
+            self.GetAlarmAdditionalInfoCommand(self, self.op_state_model, self.logger)
         )
         self.register_command_object(
             "GetAlarmStats",
-            self.GetAlarmStatsCommand(self, self.state_model, self.logger)
+            self.GetAlarmStatsCommand(self, self.op_state_model, self.logger)
         )
         self.register_command_object(
             "GetAlertStats",
-            self.GetAlertStatsCommand(self, self.state_model, self.logger)
+            self.GetAlertStatsCommand(self, self.op_state_model, self.logger)
         )
 
     def always_executed_hook(self):
diff --git a/src/ska_tango_base/base/__init__.py b/src/ska_tango_base/base/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6934169135c523f121f17479ad36a08cadecad4b
--- /dev/null
+++ b/src/ska_tango_base/base/__init__.py
@@ -0,0 +1,23 @@
+"""
+This subpackage implements Tango device functionality common to all SKA
+Tango devices.
+"""
+
+__all__ = (
+    "AdminModeModel",
+    "OpStateModel",
+    "BaseComponentManager",
+    "ReferenceBaseComponentManager",
+    "check_communicating",
+    "SKABaseDevice",
+)
+
+# Note: order of imports is important - start with lowest in the hierarchy
+from .admin_mode_model import AdminModeModel
+from .op_state_model import OpStateModel
+
+from .component_manager import BaseComponentManager
+from .reference_component_manager import (
+    ReferenceBaseComponentManager, check_communicating
+)
+from .base_device import SKABaseDevice
diff --git a/src/ska_tango_base/base/admin_mode_model.py b/src/ska_tango_base/base/admin_mode_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f4f1945b50665f4a17a0a8bd610cc7bcb476f37
--- /dev/null
+++ b/src/ska_tango_base/base/admin_mode_model.py
@@ -0,0 +1,245 @@
+"""
+This module specifies the admin mode model for SKA LMC Tango devices. It
+consists of a single public class: :py:class:`.AdminModeModel`. This
+uses a state machine to device device adminMode, represented as a
+:py:class:`ska_tango_base.control_model.AdminMode` enum value, and
+reported by Tango devices through the ``AdminMode`` attribute.
+"""
+from transitions.extensions import LockedMachine as Machine
+
+from ska_tango_base.control_model import AdminMode
+from ska_tango_base.faults import StateModelError
+from ska_tango_base.utils import for_testing_only
+
+
+__all__ = ["AdminModeModel"]
+
+
+class _AdminModeMachine(Machine):
+    """
+    The state machine governing admin modes.
+
+    For documentation of states and transitions, see the documentation
+    of the public :py:class:`.AdminModeModel` class.
+    """
+
+    def __init__(self, callback=None, **extra_kwargs):
+        """
+        Initialises the admin mode state machine model.
+
+        :param callback: A callback to be called whenever there is a
+            transition to a new admin mode value
+        :type callback: callable
+        :param extra_kwargs: Additional keywords arguments to pass to
+            super class initialiser (useful for graphing)
+        """
+        self._callback = callback
+
+        states = ["RESERVED", "NOT_FITTED", "OFFLINE", "MAINTENANCE", "ONLINE"]
+        transitions = [
+            {
+                "source": ["NOT_FITTED", "RESERVED", "OFFLINE"],
+                "trigger": "to_reserved",
+                "dest": "RESERVED",
+            },
+            {
+                "source": ["RESERVED", "NOT_FITTED", "OFFLINE"],
+                "trigger": "to_notfitted",
+                "dest": "NOT_FITTED",
+            },
+            {
+                "source": [
+                    "RESERVED",
+                    "NOT_FITTED",
+                    "OFFLINE",
+                    "MAINTENANCE",
+                    "ONLINE",
+                ],
+                "trigger": "to_offline",
+                "dest": "OFFLINE",
+            },
+            {
+                "source": ["OFFLINE", "MAINTENANCE", "ONLINE"],
+                "trigger": "to_maintenance",
+                "dest": "MAINTENANCE",
+            },
+            {
+                "source": ["OFFLINE", "MAINTENANCE", "ONLINE"],
+                "trigger": "to_online",
+                "dest": "ONLINE",
+            },
+        ]
+
+        super().__init__(
+            states=states,
+            initial="OFFLINE",
+            transitions=transitions,
+            after_state_change=self._state_changed,
+            **extra_kwargs,
+        )
+        self._state_changed()
+
+    def _state_changed(self):
+        """
+        State machine callback that is called every time the admin mode
+        changes. Responsible for ensuring that callbacks are called.
+        """
+        if self._callback is not None:
+            self._callback(self.state)
+
+
+class AdminModeModel:
+    """
+    This class implements the state model for device adminMode.
+
+    The model supports the five admin modes defined by the values of the
+    :py:class:`ska_tango_base.control_model.AdminMode` enum.
+
+    * **NOT_FITTED**: the component that the device is supposed to
+      monitor is not fitted.
+    * **RESERVED**: the component that the device is monitoring is not
+      in use because it is redundant to other devices. It is ready to
+      take over should other devices fail.
+    * **OFFLINE**: the component that the device is monitoring device
+      has been declared by SKA operations not to be used
+    * **MAINTENANCE**: the component that the device is monitoring
+      cannot be used for science purposes but can be for engineering /
+      maintenance purposes, such as testing, debugging, etc.
+    * **ONLINE**: the component that the device is monitoring can be
+      used for science purposes.
+
+    The admin mode state machine allows for:
+
+    * any transition between the modes NOT_FITTED, RESERVED and OFFLINE
+      (e.g. an unfitted device being fitted as a redundant or
+      non-redundant device, a redundant device taking over when another
+      device fails, etc.)
+    * any transition between the modes OFFLINE, MAINTENANCE and ONLINE
+      (e.g. an online device being taken offline or put into maintenance
+      mode to diagnose a fault, a faulty device moving between
+      maintenance and offline mode as it undergoes sporadic periods of
+      diagnosis.)
+
+    The actions supported are:
+    
+    * **to_not_fitted**
+    * **to_reserved**
+    * **to_offline**
+    * **to_maintenance**
+    * **to_online**
+
+    A diagram of the admin mode model, as designed, is shown below
+
+    .. uml:: admin_mode_model.uml
+       :caption: Diagram of the admin mode model
+
+    The following is an diagram of the underlying state machine,
+    automatically generated from the code. Its equivalence to the
+    diagram above demonstrates that the implementation is faithful to
+    the design.
+
+    .. figure:: _AdminModeMachine_autogenerated.png
+      :alt: Diagram of the admin mode state machine, as implemented
+
+    """
+
+    def __init__(self, logger, callback=None):
+        """
+        Initialises the state model.
+
+        :param logger: the logger to be used by this state model.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param callback: A callback to be called when the state machine
+            for admin_mode reports a change of state
+        :type callback: callable
+        """
+        self.logger = logger
+
+        self._admin_mode = None
+        self._callback = callback
+
+        self._admin_mode_machine = _AdminModeMachine(callback=self._admin_mode_changed)
+
+    @property
+    def admin_mode(self):
+        """
+        Returns the admin_mode
+
+        :returns: admin_mode of this state model
+        :rtype: AdminMode
+        """
+        return self._admin_mode
+
+    def _admin_mode_changed(self, machine_state):
+        """
+        Helper method that updates admin_mode whenever the admin_mode
+        state machine reports a change of state, ensuring that the
+        callback is called if one exists.
+
+        :param machine_state: the new state of the adminMode state
+            machine
+        :type machine_state: str
+        """
+        admin_mode = AdminMode[machine_state]
+        if self._admin_mode != admin_mode:
+            self._admin_mode = admin_mode
+            if self._callback is not None:
+                self._callback(admin_mode)
+
+    def is_action_allowed(self, action, raise_if_disallowed=False):
+        """
+        Whether a given action is allowed in the current state.
+
+        :param action: an action, as given in the transitions table
+        :type action: str
+        :param raise_if_disallowed: whether to raise an exception if the
+            action is disallowed, or merely return False (optional,
+            defaults to False)
+        :type raise_if_disallowed: bool
+
+        :raises StateModelError: if the action is unknown to the state
+            machine
+
+        :return: whether the action is allowed in the current state
+        :rtype: bool
+        """
+        if action in self._admin_mode_machine.get_triggers(
+            self._admin_mode_machine.state
+        ):
+            return True
+
+        if raise_if_disallowed:
+            raise StateModelError(
+                f"Action {action} is not allowed in admin mode {self.admin_mode}."
+            )
+        return False
+
+    def perform_action(self, action):
+        """
+        Performs an action on the state model
+
+        :param action: an action, as given in the transitions table
+        :type action: str
+        """
+        _ = self.is_action_allowed(action, raise_if_disallowed=True)
+        self._admin_mode_machine.trigger(action)
+
+    @for_testing_only
+    def _straight_to_state(self, *, admin_mode):
+        """
+        Takes this AdminMode state model straight to the specified
+        AdminMode.
+
+        This method exists to simplify testing; for example, if testing
+        that a command may be run in a given AdminMode, you can push
+        this state model straight to that AdminMode, rather than having
+        to drive it to that state through a sequence of actions. It is
+        not intended that this method would be called outside of test
+        setups. A warning will be raised if it is.
+
+        :param admin_mode: the target admin mode
+        :type admin_mode:
+            :py:class:`~ska_tango_base.control_model.AdminMode`
+        """
+        getattr(self._admin_mode_machine, f"to_{admin_mode.name}")()
diff --git a/src/ska_tango_base/base_device.py b/src/ska_tango_base/base/base_device.py
similarity index 73%
rename from src/ska_tango_base/base_device.py
rename to src/ska_tango_base/base/base_device.py
index 008e1f8bcde97ffc8d4246678e10bb915da41a74..53b07a1b148701d3d9366f978c4956f7de02a7ce 100644
--- a/src/ska_tango_base/base_device.py
+++ b/src/ska_tango_base/base/base_device.py
@@ -34,17 +34,18 @@ from tango.server import run, Device, attribute, command, device_property
 import debugpy
 import ska_ser_logging
 from ska_tango_base import release
+from ska_tango_base.base import (
+    AdminModeModel, OpStateModel, BaseComponentManager
+)
 from ska_tango_base.commands import (
-    ActionCommand, BaseCommand, ResultCode
+    BaseCommand, CompletionCommand, StateModelCommand, ResponseCommand, ResultCode
 )
 from ska_tango_base.control_model import (
     AdminMode, ControlMode, SimulationMode, TestMode, HealthState,
     LoggingLevel
 )
-from ska_tango_base.faults import StateModelError
-from ska_tango_base.state_machine import OperationStateMachine, AdminModeStateMachine
 
-from ska_tango_base.utils import get_groups_from_json, for_testing_only
+from ska_tango_base.utils import get_groups_from_json
 from ska_tango_base.faults import GroupDefinitionsError, LoggingTargetError, LoggingLevelError
 
 LOG_FILE_SIZE = 1024 * 1024  # Log file size 1MB.
@@ -52,7 +53,7 @@ _DEBUGGER_PORT = 5678
 
 
 class _Log4TangoLoggingLevel(enum.IntEnum):
-    """Python enumerated type for TANGO log4tango logging levels.
+    """Python enumerated type for Tango log4tango logging levels.
 
     This is different to tango.LogLevel, and is required if using
     a device's set_log_level() method.  It is not currently exported
@@ -145,7 +146,7 @@ class LoggingUtils:
             Empty and whitespace-only strings are ignored.  Can also be None.
 
         :param device_name:
-            TANGO device name, like 'domain/family/member', used
+            Tango device name, like 'domain/family/member', used
             for the default file name
 
         :return: list of '<type>::<name>' strings, with default name, if applicable
@@ -316,246 +317,7 @@ class LoggingUtils:
 # PROTECTED REGION END #    //  SKABaseDevice.additionnal_import
 
 
-__all__ = ["DeviceStateModel", "SKABaseDevice", "main"]
-
-
-class DeviceStateModel:
-    """
-    Implements the state model for the SKABaseDevice.
-
-    This implementation contains separate state machines for adminMode
-    and opState. Since the two are slightly but inextricably coupled,
-    the opState machine includes "ADMIN" flavours for the "INIT",
-    "FAULT" and "DISABLED" states, to represent states where the device
-    has been administratively disabled via the adminModes "RESERVED",
-    "NOT_FITTED" and "OFFLINE". This model drives the two state machines
-    to ensure they remain coherent.
-    """
-
-    def __init__(self, logger, op_state_callback=None, admin_mode_callback=None):
-        """
-        Initialises the state model.
-
-        :param logger: the logger to be used by this state model.
-        :type logger: a logger that implements the standard library
-            logger interface
-        :param op_state_callback: A callback to be called when the state
-            machine for op_state reports a change of state
-        :type op_state_callback: callable
-        :param admin_mode_callback: A callback to be called when the
-            state machine for admin_mode reports a change of state
-        :type admin_mode_callback: callable
-        """
-        self.logger = logger
-
-        self._op_state = None
-        self._admin_mode = None
-
-        self._op_state_callback = op_state_callback
-        self._admin_mode_callback = admin_mode_callback
-
-        self._op_state_machine = OperationStateMachine(callback=self._update_op_state)
-        self._admin_mode_state_machine = AdminModeStateMachine(
-            callback=self._update_admin_mode
-        )
-
-    @property
-    def admin_mode(self):
-        """
-        Returns the admin_mode
-
-        :returns: admin_mode of this state model
-        :rtype: AdminMode
-        """
-        return self._admin_mode
-
-    def _update_admin_mode(self, machine_state):
-        """
-        Helper method that updates admin_mode whenever the admin_mode
-        state machine reports a change of state, ensuring that the
-        callback is called if one exists.
-
-        :param machine_state: the new state of the adminMode state
-            machine
-        :type machine_state: str
-        """
-        admin_mode = AdminMode[machine_state]
-        if self._admin_mode != admin_mode:
-            self._admin_mode = admin_mode
-            if self._admin_mode_callback is not None:
-                self._admin_mode_callback(admin_mode)
-
-    @property
-    def op_state(self):
-        """
-        Returns the op_state of this state model
-
-        :returns: op_state of this state model
-        :rtype: tango.DevState
-        """
-        return self._op_state
-
-    _op_state_mapping = {
-        "INIT": DevState.INIT,
-        "INIT_ADMIN": DevState.INIT,
-        "FAULT": DevState.FAULT,
-        "FAULT_ADMIN": DevState.FAULT,
-        "DISABLE": DevState.DISABLE,
-        "DISABLE_ADMIN": DevState.DISABLE,
-        "STANDBY": DevState.STANDBY,
-        "OFF": DevState.OFF,
-        "ON": DevState.ON,
-    }
-
-    def _update_op_state(self, machine_state):
-        """
-        Helper method that updates op_state whenever the operation
-        state machine reports a change of state, ensuring that the
-        callback is called if one exists.
-
-        :param machine_state: the new state of the operation state
-            machine
-        :type machine_state: str
-        """
-        op_state = self._op_state_mapping[machine_state]
-        if self._op_state != op_state:
-            self._op_state = op_state
-            if self._op_state_callback is not None:
-                self._op_state_callback(op_state)
-
-    __action_breakdown = {
-        # "action": ("action_on_op_machine", "action_on_admin_mode_machine"),
-        "to_reserved": ("admin_on", "to_reserved"),
-        "to_notfitted": ("admin_on", "to_notfitted"),
-        "to_offline": ("admin_on", "to_offline"),
-        "to_maintenance": ("admin_off", "to_maintenance"),
-        "to_online": ("admin_off", "to_online"),
-        "init_started": ("init_started", None),
-        "init_succeeded_disable": ("init_succeeded_disable", None),
-        "init_succeeded_standby": ("init_succeeded_standby", None),
-        "init_succeeded_off": ("init_succeeded_off", None),
-        "init_failed": ("init_failed", None),
-        "reset_started": ("reset_started", None),
-        "reset_succeeded_disable": ("reset_succeeded_disable", None),
-        "reset_succeeded_standby": ("reset_succeeded_standby", None),
-        "reset_succeeded_off": ("reset_succeeded_off", None),
-        "reset_failed": ("reset_failed", None),
-        "disable_succeeded": ("disable_succeeded", None),
-        "disable_failed": ("disable_failed", None),
-        "standby_succeeded": ("standby_succeeded", None),
-        "standby_failed": ("standby_failed", None),
-        "off_succeeded": ("off_succeeded", None),
-        "off_failed": ("off_failed", None),
-        "on_succeeded": ("on_succeeded", None),
-        "on_failed": ("on_failed", None),
-        "fatal_error": ("fatal_error", None),
-    }
-
-    def is_action_allowed(self, action):
-        """
-        Whether a given action is allowed in the current state.
-
-        :param action: an action, as given in the transitions table
-        :type action: str
-
-        :raises StateModelError: if the action is unknown to the state
-            machine
-
-        :return: whether the action is allowed in the current state
-        :rtype: bool
-        """
-        try:
-            (op_action, admin_action) = self.__action_breakdown[action]
-        except KeyError as key_error:
-            raise StateModelError(key_error)
-
-        if (
-            admin_action is not None
-            and admin_action
-            not in self._admin_mode_state_machine.get_triggers(
-                self._admin_mode_state_machine.state
-            )
-        ):
-            return False
-        return op_action in self._op_state_machine.get_triggers(
-            self._op_state_machine.state
-        )
-
-    def try_action(self, action):
-        """
-        Checks whether a given action is allowed in the current state,
-        and raises a StateModelError if it is not.
-
-        :param action: an action, as given in the transitions table
-        :type action: str
-
-        :raises StateModelError: if the action is not allowed in the
-            current state
-
-        :returns: True if the action is allowed
-        :rtype: boolean
-        """
-        if not self.is_action_allowed(action):
-            raise StateModelError(
-                f"Action {action} is not allowed in operational state "
-                f"{self.op_state}, admin mode {self.admin_mode}."
-            )
-        return True
-
-    def perform_action(self, action):
-        """
-        Performs an action on the state model
-
-        :param action: an action, as given in the transitions table
-        :type action: ANY
-        :raises StateModelError: if the action is not allowed in the
-            current state
-
-        """
-        self.try_action(action)
-
-        (op_action, admin_action) = self.__action_breakdown[action]
-
-        if op_action is not None:
-            self._op_state_machine.trigger(op_action)
-        if admin_action is not None:
-            self._admin_mode_state_machine.trigger(action)
-
-    @for_testing_only
-    def _straight_to_state(self, op_state=None, admin_mode=None):
-        """
-        Takes the DeviceStateModel straight to the specified state / mode. This
-        method exists to simplify testing; for example, if testing that a command
-        may be run in a given state, one can push the state model straight to that
-        state, rather than having to drive it to that state through a sequence
-        of actions. It is not intended that this method would be called outside
-        of test setups. A warning will be raised if it is.
-
-        Note that this method will allow you to put the device into an incoherent
-        combination of admin_mode and op_state (e.g. OFFLINE and ON).
-
-        :param op_state: the target operational state (optional)
-        :type op_state: :py:class:`tango.DevState`
-        :param admin_mode: the target admin mode (optional)
-        :type admin_mode: :py:class:`~ska_tango_base.control_model.AdminMode`
-        """
-        if admin_mode is None:
-            admin_mode = self._admin_mode_state_machine.state
-        else:
-            admin_mode = admin_mode.name
-
-        if op_state is None:
-            op_state = self._op_state_machine.state
-        else:
-            op_state = op_state.name
-
-        if op_state.endswith("_ADMIN"):
-            op_state = op_state[:-6]
-        if admin_mode in ["RESERVED", "NOT_FITTED", "OFFLINE"]:
-            op_state = f"{op_state}_ADMIN"
-
-        getattr(self._admin_mode_state_machine, f"to_{admin_mode}")()
-        getattr(self._op_state_machine, f"to_{op_state}")()
+__all__ = ["SKABaseDevice", "main"]
 
 
 class SKABaseDevice(Device):
@@ -565,31 +327,29 @@ class SKABaseDevice(Device):
 
     _global_debugger_listening = False
 
-    class InitCommand(ActionCommand):
+    class InitCommand(ResponseCommand, CompletionCommand):
         """
         A class for the SKABaseDevice's init_device() "command".
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, logger=None):
             """
             Create a new InitCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
+                example, the SKABaseDevice device for which this class
                 implements the command
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`DeviceStateModel`
+            :param op_state_model: the state model that this command
+                 uses to check that it is allowed to run, and that it
+                 drives with actions.
+            :type op_state_model: :py:class:`OpStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(
-                target, state_model, "init", start_action=True, logger=logger
-            )
+            super().__init__(target, op_state_model, "init", logger=logger)
 
         def do(self):
             """
@@ -621,7 +381,7 @@ class SKABaseDevice(Device):
             device._methods_patched_for_debugger = False
 
             try:
-                # create TANGO Groups dict, according to property
+                # create Tango Groups dict, according to property
                 self.logger.debug(
                     "Groups definitions: {}".format(
                         device.GroupDefinitions
@@ -646,9 +406,6 @@ class SKABaseDevice(Device):
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-        def succeeded(self):
-            self.state_model.perform_action("init_succeeded_off")
-
     _logging_config_lock = threading.Lock()
     _logging_configured = False
 
@@ -702,7 +459,7 @@ class SKABaseDevice(Device):
         self.write_loggingTargets(self.LoggingTargetsDefault)
         self.logger.debug('Logger initialised')
 
-        # monkey patch TANGO Logging Service streams so they go to the Python
+        # monkey patch Tango Logging Service streams so they go to the Python
         # logger instead
         self.debug_stream = self.logger.debug
         self.info_stream = self.logger.info
@@ -733,7 +490,7 @@ class SKABaseDevice(Device):
     Device property.
        
     Each string in the list is a JSON serialised dict defining the ``group_name``,
-    ``devices`` and ``subgroups`` in the group.  A TANGO Group object is created
+    ``devices`` and ``subgroups`` in the group.  A Tango Group object is created
     for each item in the list, according to the hierarchy defined.  This provides
     easy access to the managed devices in bulk, or individually.
     
@@ -841,6 +598,7 @@ class SKABaseDevice(Device):
         dtype=AdminMode,
         access=AttrWriteType.READ_WRITE,
         memorized=True,
+        hw_memorized=True,
         doc="The admin mode reported for this device. It may interpret the current "
             "device condition and condition of all managed devices to set this. "
             "Most possibly an aggregate attribute.",
@@ -851,8 +609,9 @@ class SKABaseDevice(Device):
         dtype=ControlMode,
         access=AttrWriteType.READ_WRITE,
         memorized=True,
+        hw_memorized=True,
         doc="The control mode of the device. REMOTE, LOCAL"
-            "\nTANGO Device accepts only from a ‘local’ client and ignores commands and "
+            "\nTango Device accepts only from a ‘local’ client and ignores commands and "
             "queries received from TM or any other ‘remote’ clients. The Local clients"
             " has to release LOCAL control before REMOTE clients can take control again.",
     )
@@ -862,6 +621,7 @@ class SKABaseDevice(Device):
         dtype=SimulationMode,
         access=AttrWriteType.READ_WRITE,
         memorized=True,
+        hw_memorized=True,
         doc="Reports the simulation mode of the device. \nSome devices may implement "
             "both modes, while others will have simulators that set simulationMode "
             "to True while the real devices always set simulationMode to False.",
@@ -872,6 +632,7 @@ class SKABaseDevice(Device):
         dtype=TestMode,
         access=AttrWriteType.READ_WRITE,
         memorized=True,
+        hw_memorized=True,
         doc="The test mode of the device. \n"
             "Either no test mode or an "
             "indication of the test mode.",
@@ -944,11 +705,8 @@ class SKABaseDevice(Device):
 
             self._init_logging()
             self._init_state_model()
-
-            self._command_objects = {}
-
-            self.InitCommand(self, self.state_model, self.logger)()
-
+            self.component_manager = self.create_component_manager()
+            self.InitCommand(self, self.op_state_model, self.logger)()
             self.init_command_objects()
         except Exception as exc:
             self.set_state(DevState.FAULT)
@@ -962,11 +720,17 @@ class SKABaseDevice(Device):
         """
         Creates the state model for the device
         """
-        self.state_model = DeviceStateModel(
+        self.op_state_model = OpStateModel(
             logger=self.logger,
-            op_state_callback=self._update_state,
-            admin_mode_callback=self._update_admin_mode,
+            callback=self._update_state,
         )
+        self.admin_mode_model = AdminModeModel(
+            logger=self.logger,
+            callback = self._update_admin_mode,
+        )
+
+    def create_component_manager(self):
+        return BaseComponentManager(self.op_state_model)
 
     def register_command_object(self, command_name, command_object):
         """
@@ -1000,13 +764,15 @@ class SKABaseDevice(Device):
         Creates and registers command objects (handlers) for the
         commands supported by this device.
         """
-        device_args = (self, self.state_model, self.logger)
+        self._command_objects = {}
 
-        self.register_command_object("Disable", self.DisableCommand(*device_args))
-        self.register_command_object("Standby", self.StandbyCommand(*device_args))
-        self.register_command_object("Off", self.OffCommand(*device_args))
-        self.register_command_object("On", self.OnCommand(*device_args))
-        self.register_command_object("Reset", self.ResetCommand(*device_args))
+        component_args = (self.component_manager, self.op_state_model, self.logger)
+        self.register_command_object("Standby", self.StandbyCommand(*component_args))
+        self.register_command_object("Off", self.OffCommand(*component_args))
+        self.register_command_object("On", self.OnCommand(*component_args))
+        self.register_command_object("Reset", self.ResetCommand(*component_args))
+
+        device_args = (self, self.op_state_model, self.logger)
         self.register_command_object(
             "GetVersionInfo", self.GetVersionInfoCommand(*device_args)
         )
@@ -1133,7 +899,7 @@ class SKABaseDevice(Device):
         :return: Admin Mode of the device
         :rtype: AdminMode
         """
-        return self.state_model.admin_mode
+        return self.admin_mode_model.admin_mode
         # PROTECTED REGION END #    //  SKABaseDevice.adminMode_read
 
     def write_adminMode(self, value):
@@ -1147,15 +913,21 @@ class SKABaseDevice(Device):
         :raises ValueError: for unknown adminMode
         """
         if value == AdminMode.NOT_FITTED:
-            self.state_model.perform_action("to_notfitted")
+            self.admin_mode_model.perform_action("to_notfitted")
         elif value == AdminMode.OFFLINE:
-            self.state_model.perform_action("to_offline")
+            self.admin_mode_model.perform_action("to_offline")
+            if self.component_manager.is_communicating:
+                self.component_manager.stop_communicating()
         elif value == AdminMode.MAINTENANCE:
-            self.state_model.perform_action("to_maintenance")
+            self.admin_mode_model.perform_action("to_maintenance")
+            if not self.component_manager.is_communicating:
+                self.component_manager.start_communicating()
         elif value == AdminMode.ONLINE:
-            self.state_model.perform_action("to_online")
+            self.admin_mode_model.perform_action("to_online")
+            if not self.component_manager.is_communicating:
+                self.component_manager.start_communicating()
         elif value == AdminMode.RESERVED:
-            self.state_model.perform_action("to_reserved")
+            self.admin_mode_model.perform_action("to_reserved")
         else:
             raise ValueError(f"Unknown adminMode {value}")
         # PROTECTED REGION END #    //  SKABaseDevice.adminMode_write
@@ -1226,7 +998,7 @@ class SKABaseDevice(Device):
 
     class GetVersionInfoCommand(BaseCommand):
         """
-        A class for the SKABaseDevice's Reset() command.
+        A class for the SKABaseDevice's GetVersionInfo() command.
         """
 
         def do(self):
@@ -1257,54 +1029,28 @@ class SKABaseDevice(Device):
         return command()
         # PROTECTED REGION END #    //  SKABaseDevice.GetVersionInfo
 
-    class ResetCommand(ActionCommand):
+    class ResetCommand(StateModelCommand, ResponseCommand):
         """
         A class for the SKABaseDevice's Reset() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, logger=None):
             """
             Create a new ResetCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`DeviceStateModel`
+            :param op_state_model: the state model that this command
+                uses to check that it is allowed to run, and that it
+                drives with actions.
+            :type op_state_model: :py:class:`OpStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "reset", logger=logger)
-
-        def check_allowed(self):
-            """
-            Checks whether the command is allowed to be run in the current
-            state of the state model.
-
-            :returns: True if the command is allowed to be run
-            """
-            return self._try_action("reset_succeeded_off")
-
-        def is_allowed(self):
-            """
-            Whether this command is allowed to run in the current state of
-            the state model.
-
-            :returns: whether this command is allowed to run
-            :rtype: boolean
-            """
-            return self._is_action_allowed("reset_succeeded_off")
-
-        def succeeded(self):
-            """
-            Action to take on successful completion of a reset
-            """
-            self.state_model.perform_action("reset_succeeded_off")
+            super().__init__(target, op_state_model, "reset", logger=logger)
 
         def do(self):
             """
@@ -1315,12 +1061,7 @@ class SKABaseDevice(Device):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-            device._health_state = HealthState.OK
-            device._control_mode = ControlMode.REMOTE
-            device._simulation_mode = SimulationMode.FALSE
-            device._test_mode = TestMode.NONE
-
+            self.target.reset()
             message = "Reset command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
@@ -1335,7 +1076,7 @@ class SKABaseDevice(Device):
         :rtype: boolean
         """
         command = self.get_command_object("Reset")
-        return command.is_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1358,99 +1099,28 @@ class SKABaseDevice(Device):
         (return_code, message) = command()
         return [[return_code], [message]]
 
-    class DisableCommand(ActionCommand):
-        """
-        A class for the SKABaseDevice's Disable() command.
-        """
-
-        def __init__(self, target, state_model, logger=None):
-            """
-            Constructor for DisableCommand
-
-            :param target: the object that this command acts upon; for
-                example, the SKABaseDevice for which this class
-                implements the command
-            :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`DeviceStateModel`
-            :param logger: the logger to be used by this Command. If not
-                provided, then a default module logger will be used.
-            :type logger: a logger that implements the standard library
-                logger interface
-            """
-            super().__init__(target, state_model, "disable", logger=logger)
-
-        def do(self):
-            """
-            Stateless hook for Disable() command functionality.
-
-            :return: A tuple containing a return code and a string
-                message indicating status. The message is for
-                information purpose only.
-            :rtype: (ResultCode, str)
-            """
-            message = "Disable command completed OK"
-            self.logger.info(message)
-            return (ResultCode.OK, message)
-
-    def is_Disable_allowed(self):
-        """
-        Check if command Disable is allowed in the current device state.
-
-        :raises ``tango.DevFailed``: if the command is not allowed
-
-        :return: ``True`` if the command is allowed
-        :rtype: boolean
-        """
-        command = self.get_command_object("Disable")
-        return command.check_allowed()
-
-    @command(
-        dtype_out="DevVarLongStringArray",
-        doc_out="(ReturnType, 'informational message')",
-    )
-    @DebugIt()
-    def Disable(self):
-        """
-        Put the device into disabled mode
-
-        To modify behaviour for this command, modify the do() method of
-        the command class.
-
-        :return: A tuple containing a return code and a string
-            message indicating status. The message is for
-            information purpose only.
-        :rtype: (ResultCode, str)
-        """
-        command = self.get_command_object("Disable")
-        (return_code, message) = command()
-        return [[return_code], [message]]
-
-    class StandbyCommand(ActionCommand):
+    class StandbyCommand(StateModelCommand, ResponseCommand):
         """
         A class for the SKABaseDevice's Standby() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, logger=None):
             """
             Constructor for StandbyCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKABaseDevice for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
+            :param op_state_model: the state model that this command uses
                  to check that it is allowed to run, and that it drives
                  with actions.
-            :type state_model: :py:class:`DeviceStateModel`
+            :type op_state_model: :py:class:`OpStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "standby", logger=logger)
+            super().__init__(target, op_state_model, "standby", logger=logger)
 
         def do(self):
             """
@@ -1461,6 +1131,7 @@ class SKABaseDevice(Device):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            self.target.standby()
             message = "Standby command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
@@ -1469,13 +1140,13 @@ class SKABaseDevice(Device):
         """
         Check if command Standby is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
+        :raises :py:exc:`CommandError`: if the command is not allowed
 
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Standby")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1498,29 +1169,28 @@ class SKABaseDevice(Device):
         (return_code, message) = command()
         return [[return_code], [message]]
 
-    class OffCommand(ActionCommand):
+    class OffCommand(StateModelCommand, ResponseCommand):
         """
         A class for the SKABaseDevice's Off() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, logger=None):
             """
             Constructor for OffCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKABaseDevice for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
+            :param op_state_model: the state model that this command uses
                  to check that it is allowed to run, and that it drives
                  with actions.
-            :type state_model: :py:class:`DeviceStateModel`
+            :type op_state_model: :py:class:`OpStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "off", logger=logger)
+            super().__init__(target, op_state_model, "off", logger=logger)
 
         def do(self):
             """
@@ -1531,6 +1201,7 @@ class SKABaseDevice(Device):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            self.target.off()
             message = "Off command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
@@ -1539,13 +1210,13 @@ class SKABaseDevice(Device):
         """
         Check if command `Off` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
+        :raises :py:exc:`CommandError`: if the command is not allowed
 
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Off")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1568,29 +1239,28 @@ class SKABaseDevice(Device):
         (return_code, message) = command()
         return [[return_code], [message]]
 
-    class OnCommand(ActionCommand):
+    class OnCommand(StateModelCommand, ResponseCommand):
         """
         A class for the SKABaseDevice's On() command.
         """
-
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, logger=None):
             """
             Constructor for OnCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKABaseDevice for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
+            :param op_state_model: the state model that this command uses
                  to check that it is allowed to run, and that it drives
                  with actions.
-            :type state_model: :py:class:`DeviceStateModel`
+            :type op_state_model: :py:class:`OpStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "on", logger=logger)
+            super().__init__(target, op_state_model, "on", logger=logger)
+
 
         def do(self):
             """
@@ -1601,6 +1271,7 @@ class SKABaseDevice(Device):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            self.target.on()
             message = "On command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
@@ -1609,13 +1280,14 @@ class SKABaseDevice(Device):
         """
         Check if command `On` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
+        :raises :py:exc:`CommandError`: if the command is not
+            allowed
 
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("On")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out="DevVarLongStringArray",
diff --git a/src/ska_tango_base/base/component_manager.py b/src/ska_tango_base/base/component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7535685c6ed37c30ce0ba5a6c771f22c388ce45
--- /dev/null
+++ b/src/ska_tango_base/base/component_manager.py
@@ -0,0 +1,159 @@
+"""
+This module provides an abstract component manager for SKA Tango base devices.
+
+The basic model is:
+
+* Every Tango device has a *component* that it monitors and/or
+  controls. That component could be, for example:
+
+  * Hardware such as an antenna, APIU, TPM, switch, subrack, etc.
+
+  * An external software system such as a cluster manager
+
+  * A software routine, possibly implemented within the Tango device
+    itself
+    
+  * In a hierarchical system, a pool of lower-level Tango devices.
+
+* A Tango device will usually need to establish and maintain a
+  *connection* to its component. This connection may be deliberately
+  broken by the device, or it may fail.
+
+* A Tango device *controls* its component by issuing commands that cause
+  the component to change behaviour and/or state; and it *monitors* its
+  component by keeping track of its state.
+"""
+from ska_tango_base.control_model import PowerMode
+
+
+class BaseComponentManager:
+    """
+    An abstract base class for a component manager for SKA Tango
+    devices, supporting:
+
+    * Maintaining a connection to its component
+
+    * Controlling its component via commands like Off(), Standby(),
+      On(), etc.
+
+    * Monitoring its component, e.g. detect that it has been turned off
+      or on
+    """
+
+    def __init__(self, op_state_model, *args, **kwargs):
+        """
+        Initialise a new ComponentManager instance
+
+        :param op_state_model: the op state model used by this component
+            manager
+        """
+        self.op_state_model = op_state_model
+
+    def start_communicating(self):
+        """
+        Establish communication with the component, then start
+        monitoring. This is the place to do things like:
+        
+        * Initiate a connection to the component (if your communication
+          is connection-oriented)
+        * Subscribe to component events (if using "pull" model)
+        * Start a polling loop to monitor the component (if using a
+          "push" model)
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    def stop_communicating(self):
+        """
+        Cease monitoring the component, and break off all communication
+        with it. For example,
+
+        * If you are communicating over a connection, disconnect.
+        * If you have subscribed to events, unsubscribe.
+        * If you are running a polling loop, stop it.
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    @property
+    def is_communicating(self):
+        """
+        Whether communication with the component is established and
+        active, allowing monitoring and control of the component. For
+        example:
+        
+        * If communication is over a connection, are you connected?
+        * If communication is via event subscription, are you
+          subscribed, and is the event subsystem healthy?
+        * If you are polling the component, is the polling loop running,
+          and is the component responsive?
+
+        :return: whether there is currently a connection to the
+            component
+        :rtype: bool
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    @property
+    def power_mode(self):
+        """
+        Power mode of the component
+
+        :return: the power mode of the component
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    @property
+    def faulty(self):
+        """
+        Whether the component is currently faulting
+
+        :return: whether the component is faulting
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    def off(self):
+        """
+        Turn the component off
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    def standby(self):
+        """
+        Put the component into low-power standby mode
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    def on(self):
+        """
+        Turn the component on
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    def reset(self):
+        """
+        Reset the component (from fault state)
+        """
+        raise NotImplementedError("BaseComponentManager is abstract.")
+
+    action_map = {
+        PowerMode.OFF: "component_off",
+        PowerMode.STANDBY: "component_standby",
+        PowerMode.ON: "component_on",
+    }
+
+    def component_power_mode_changed(self, power_mode):
+        """
+        Callback hook, called when whether the component power mode
+        changes
+
+        :param power_mode: the new power mode of the component
+        :type power_mode:
+            :py:class:`ska_tango_base.control_model.PowerMode`
+        """
+        action = self.action_map[power_mode]
+        self.op_state_model.perform_action(action)
+
+    def component_fault(self):
+        """
+        Callback hook, called when the component faults
+        """
+        self.op_state_model.perform_action("component_fault")
diff --git a/src/ska_tango_base/base/op_state_model.py b/src/ska_tango_base/base/op_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e05cb2b084f15d7a2ae86223d71b25795812804
--- /dev/null
+++ b/src/ska_tango_base/base/op_state_model.py
@@ -0,0 +1,492 @@
+"""
+This module specifies the operational state ("opState") model for SKA LMC
+Tango devices. It consists of:
+
+* an underlying state machine: :py:class:`._OpStateMachine`
+* an :py:class:`.OpStateModel` that maps state machine state to device
+  "op state". This "op state" is currently represented as a
+  :py:class:`tango.DevState` enum value, and reported using the tango
+  device's special ``state()`` method.
+"""
+from tango import DevState
+from transitions.extensions import LockedMachine as Machine
+
+from ska_tango_base.faults import StateModelError
+from ska_tango_base.utils import for_testing_only
+
+
+__all__ = ["OpStateModel"]
+
+
+class _OpStateMachine(Machine):
+    """
+    State machine representing the overall state of the device with
+    respect to system component that it monitors.
+
+    The post-init states supported are:
+
+    * **DISABLE**: the device has been told not to monitor its telescope
+      component
+    * **UNKNOWN**: the device is monitoring (or at least trying to
+      monitor) its telescope component but is unable to determine the
+      component's state
+    * **OFF**: the device is monitoring its telescope component and the
+      component is powered off
+    * **STANDBY**: the device is monitoring its telescope component and
+      the component is in low-power standby mode
+    * **ON**: the device is monitoring its telescope component and the
+      component is turned on
+    * **FAULT**: the device is monitoring its telescope component and
+      the component has failed or is in an inconsistent state.
+
+    There are also corresponding initialising states: **INIT_DISABLE**,
+    **INIT_UNKNOWN**, **INIT_OFF**, **INIT_STANDBY**, **INIT_ON** and
+    **INIT_FAULT**. These states allow for the underlying system
+    component to change its state during long-running initialisation,
+    and for a device to transition to an appropriate state at the end of
+    initialisation.
+
+    Finally, there is an **_UNINITIALISED** starting state, representing
+    a device that hasn't started initialising yet.
+
+    The actions supported are:
+
+    * **init_invoked**: the device has started initialising
+    * **init_completed**: the device has finished initialising
+    * **component_disconnected**: the device his disconnected from the
+      telescope component that it is supposed to manage (for example
+      because its admin mode was set to OFFLINE). Note, this action
+      indicates a device-initiated, deliberate disconnect; a lost
+      connection would be indicated by a "component_fault" or
+      "component_unknown** action, depending on circumstances.
+    * **component_unknown**: the device is unable to determine the state
+      of its component.
+    * **component_off**: the component has been switched off
+    * **component_standby**: the component has switched to low-power
+      standby mode
+    * **component_on**: the component has been switched on
+
+    A diagram of the state machine is shown below. Essentially, the
+    machine has three "super-states", representing a device before,
+    during and after initialisation. Transition between these
+    "super-states" is triggered by the "init_invoked" and
+    "init_completed" actions. In the last two "super-states", the device
+    monitors the component and updates its state accordingly.
+
+    .. uml:: op_state_machine.uml
+       :caption: Diagram of the op state machine
+
+    The following is a diagram of the state machine, automatically
+    generated from the code. Its equivalence to the diagram above
+    demonstrates that the implementation is faithful to the design.
+
+    .. figure:: _OpStateMachine_autogenerated.png
+      :alt: Diagram of the op state machine, as implemented
+
+    """
+
+    def __init__(self, callback=None, **extra_kwargs):
+        """
+        Initialises the state model.
+
+        :param callback: A callback to be called when a transition
+            implies a change to op state
+        :type callback: callable
+        :param extra_kwargs: Additional keywords arguments to pass to
+            superclass initialiser (useful for graphing)
+        """
+        self._callback = callback
+
+        states = [
+            "_UNINITIALISED",
+            "INIT_DISABLE",
+            "INIT_UNKNOWN",
+            "INIT_OFF",
+            "INIT_STANDBY",
+            "INIT_ON",
+            "INIT_FAULT",
+            "DISABLE",
+            "UNKNOWN",
+            "OFF",
+            "STANDBY",
+            "ON",
+            "FAULT",
+        ]
+
+        transitions = [
+            # Initial transition on the device starting initialisation
+            {
+                "source": "_UNINITIALISED",
+                "trigger": "init_invoked",
+                "dest": "INIT_DISABLE",
+            },
+            # Changes in the state of the monitored component
+            # while the device is initialising
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_disconnected",
+                "dest": "INIT_DISABLE",
+            },
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_unknown",
+                "dest": "INIT_UNKNOWN",
+            },
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_off",
+                "dest": "INIT_OFF",
+            },
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_standby",
+                "dest": "INIT_STANDBY",
+            },
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_on",
+                "dest": "INIT_ON",
+            },
+            {
+                "source": [
+                    "INIT_DISABLE",
+                    "INIT_UNKNOWN",
+                    "INIT_OFF",
+                    "INIT_STANDBY",
+                    "INIT_ON",
+                    "INIT_FAULT",
+                ],
+                "trigger": "component_fault",
+                "dest": "INIT_FAULT",
+            },
+            # Completion of initialisation
+            {
+                "source": "INIT_DISABLE",
+                "trigger": "init_completed",
+                "dest": "DISABLE",
+            },
+            {
+                "source": "INIT_UNKNOWN",
+                "trigger": "init_completed",
+                "dest": "UNKNOWN",
+            },
+            {
+                "source": "INIT_OFF",
+                "trigger": "init_completed",
+                "dest": "OFF",
+            },
+            {
+                "source": "INIT_STANDBY",
+                "trigger": "init_completed",
+                "dest": "STANDBY",
+            },
+            {
+                "source": "INIT_ON",
+                "trigger": "init_completed",
+                "dest": "ON",
+            },
+            {
+                "source": "INIT_FAULT",
+                "trigger": "init_completed",
+                "dest": "FAULT",
+            },
+            # Changes in the state of the monitored component post-initialisation
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_disconnected",
+                "dest": "DISABLE",
+            },
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_unknown",
+                "dest": "UNKNOWN",
+            },
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_off",
+                "dest": "OFF",
+            },
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_standby",
+                "dest": "STANDBY",
+            },
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_on",
+                "dest": "ON",
+            },
+            {
+                "source": ["DISABLE", "UNKNOWN", "OFF", "STANDBY", "ON", "FAULT"],
+                "trigger": "component_fault",
+                "dest": "FAULT",
+            },
+            # Transitions governing what device commands are permitted
+            {
+                "source": "FAULT",
+                "trigger": "reset_invoked",
+                "dest": "=",
+            },
+            {
+                "source": ["OFF", "STANDBY", "ON"],
+                "trigger": "off_invoked",
+                "dest": "=",
+            },
+            {
+                "source": ["OFF", "STANDBY", "ON"],
+                "trigger": "standby_invoked",
+                "dest": "=",
+            },
+            {
+                "source": ["OFF", "STANDBY", "ON"],
+                "trigger": "on_invoked",
+                "dest": "=",
+            },
+        ]
+
+        super().__init__(
+            states=states,
+            initial="_UNINITIALISED",
+            transitions=transitions,
+            after_state_change=self._state_changed,
+            **extra_kwargs,
+        )
+        self._state_changed()
+
+    def _state_changed(self):
+        """
+        State machine callback that is called every time the op_state
+        changes. Responsible for ensuring that callbacks are called.
+        """
+        if self._callback is not None:
+            self._callback(self.state)
+
+
+class OpStateModel:
+    """
+    This class implements the state model for device operational state
+    ("opState").
+
+    The model supports the following states, represented as values of
+    the :py:class:`tango.DevState` enum.
+
+    * **INIT**: the device is initialising.
+    * **DISABLE**: the device has been told not to monitor its telescope
+      component
+    * **UNKNOWN**: the device is monitoring (or at least trying to
+      monitor) its telescope component but is unable to determine the
+      component's state
+    * **OFF**: the device is monitoring its telescope component and the
+      component is powered off
+    * **STANDBY**: the device is monitoring its telescope component and
+      the component is in low-power standby mode
+    * **ON**: the device is monitoring its telescope component and the
+      component is turned on
+    * **FAULT**: the device is monitoring its telescope component and
+      the component has failed or is in an inconsistent state.
+
+    These are essentially the same states as the underlying
+    :py:class:`._OpStateMachine`, except that all initialisation states
+    are mapped to the INIT DevState.
+
+    The actions supported are:
+
+    * **init_invoked**: the device has started initialising
+    * **init_completed**: the device has finished initialising
+    * **component_disconnected**: the device his disconnected from the
+      telescope component that it is supposed to manage (for example
+      because its admin mode was set to OFFLINE). Note, this action
+      indicates a device-initiated, deliberate disconnect; a lost
+      connection would be indicated by a "component_fault" or
+      "component_unknown" action, depending on circumstances.
+    * **component_unknown**: the device is unable to determine the state
+      of its component.
+    * **component_off**: the component has been switched off
+    * **component_standby**: the component has switched to low-power
+      standby mode
+    * **component_on**: the component has been switched on
+
+    A diagram of the operational state model, as implemented, is shown
+    below.
+    
+    .. uml:: op_state_model.uml
+       :caption: Diagram of the operational state model
+
+    The following hierarchical diagram is more explanatory; however note
+    that the implementation does *not* use a hierarchical state machine.
+
+    .. uml:: op_state_model_hierarchical.uml
+       :caption: Diagram of the operational state model
+    """
+
+    def __init__(self, logger, callback=None):
+        """
+        Initialises the operational state model.
+
+        :param logger: the logger to be used by this state model.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param callback: A callback to be called when the state machine
+            for op_state reports a change of state
+        :type callback: callable
+        """
+        self.logger = logger
+
+        self._op_state = None
+        self._callback = callback
+
+        self._op_state_machine = _OpStateMachine(callback=self._op_state_changed)
+
+    @property
+    def op_state(self):
+        """
+        Returns the op state
+
+        :returns: the op state of this state model
+        :rtype: :py:class:`tango.DevState`
+        """
+        return self._op_state
+
+    _op_state_mapping = {
+        "_UNINITIALISED": None,
+        "INIT_DISABLE": DevState.INIT,
+        "INIT_UNKNOWN": DevState.INIT,
+        "INIT_OFF": DevState.INIT,
+        "INIT_STANDBY": DevState.INIT,
+        "INIT_ON": DevState.INIT,
+        "INIT_FAULT": DevState.INIT,
+        "DISABLE": DevState.DISABLE,
+        "UNKNOWN": DevState.UNKNOWN,
+        "OFF": DevState.OFF,
+        "STANDBY": DevState.STANDBY,
+        "ON": DevState.ON,
+        "FAULT": DevState.FAULT,
+    }
+
+    def _op_state_changed(self, machine_state):
+        """
+        Helper method that updates op_state whenever the operational
+        state machine reports a change of state, ensuring that the
+        callback is called if one exists.
+
+        :param machine_state: the new state of the operational state
+            machine
+        :type machine_state: str
+        """
+        op_state = self._op_state_mapping[machine_state]
+        if self._op_state != op_state:
+            self._op_state = op_state
+            if self._callback is not None:
+                self._callback(op_state)
+
+    def is_action_allowed(self, action, raise_if_disallowed=False):
+        """
+        Whether a given action is allowed in the current state.
+
+        :param action: an action, as given in the transitions table
+        :type action: str
+        :param raise_if_disallowed: whether to raise an exception if the
+            action is disallowed, or merely return False (optional,
+            defaults to False)
+        :type raise_if_disallowed: bool
+
+        :raises StateModelError: if the action is unknown to the state
+            machine
+
+        :return: whether the action is allowed in the current state
+        :rtype: bool
+        """
+        if action in self._op_state_machine.get_triggers(self._op_state_machine.state):
+            return True
+
+        if raise_if_disallowed:
+            raise StateModelError(
+                f"Action {action} is not allowed in op_state {self.op_state}."
+            )
+        return False
+
+    def perform_action(self, action):
+        """
+        Performs an action on the state model
+
+        :param action: an action, as given in the transitions table
+        :type action: str
+        """
+        _ = self.is_action_allowed(action, raise_if_disallowed=True)
+        self._op_state_machine.trigger(action)
+
+    @for_testing_only
+    def _straight_to_state(self, op_state_name):
+        """
+        Takes this op state model straight to the specified underlying
+        op state.
+
+        This method exists to simplify testing; for example, if testing
+        that a command may be run in a given op state, you can push
+        this state model straight to that op state, rather than having
+        to drive it to that state through a sequence of actions. It is
+        not intended that this method would be called outside of test
+        setups. A warning will be raised if it is.
+
+        The state must be provided as the string name of a state in the
+        underlying :py:class:`._OpStateMachine`. This machine has more
+        specific states, allowing for more flexibility in test setup.
+        Specifically, there are "DISABLE", "UNKNOWN", "OFF", "STANDBY",
+        "ON" and "FAULT" states, representing states of an initialised
+        device. But instead of a single "INIT" state, there are
+        "INIT_DISABLE", "INIT_UNKNOWN", "INIT_OFF", "INIT_STANDBY",
+        "INIT_ON" and "INIT_FAULT" states, representing an initialised
+        device with the monitored component in a given state.
+
+        For example, to test that a device transitions out of INIT into
+        STANDBY when the component that it monitors is in low-power
+        standby mode:
+
+        .. code-block:: py
+
+          model = OpStateModel(logger)
+          model._straight_to_state("INIT_STANDBY")
+          assert model.op_state == DevState.INIT
+          model.perform_action("init_completed")
+          assert model.op_state == DevState.STANDBY
+
+        :param op_state_name: the name of a target op state, as used by
+            the underlying :py:class:`._OpStateMachine`.
+        :type op_state_name: str
+        """
+        getattr(self._op_state_machine, f"to_{op_state_name}")()
diff --git a/src/ska_tango_base/base/reference_component_manager.py b/src/ska_tango_base/base/reference_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac3b0369d00b2fdf84580bb628e6882e178a294c
--- /dev/null
+++ b/src/ska_tango_base/base/reference_component_manager.py
@@ -0,0 +1,395 @@
+"""
+This module provided a reference implementation of a
+:py:class:`ska_tango_base.base.BaseComponentManager`, for
+explanatory purposes, and to support testing of this package.
+"""
+import functools
+
+from ska_tango_base.base import BaseComponentManager
+from ska_tango_base.control_model import PowerMode
+from ska_tango_base.faults import ComponentFault
+
+
+def check_communicating(func):
+    """
+    Decorator that makes a method first check that there is a connection
+    to the component, before allowing the wrapped function to proceed
+
+    :param func: the wrapped function
+
+    :return: the wrapped function
+    """
+    @functools.wraps(func)
+    def _wrapper(component_manager, *args, **kwargs):
+        """
+        Wrapper function that checks that there is a connection to the
+        component before invoking the wrapped function
+
+        :param component_manager: the component_manager to check
+        :param args: positional arguments to the wrapped function
+        :param kwargs: keyword arguments to the wrapped function
+
+        :return: whatever the wrapped function returns
+        """
+        if not component_manager.is_communicating:
+            raise ConnectionError("Not connected")
+        return func(component_manager, *args, **kwargs)
+
+    return _wrapper
+
+class ReferenceBaseComponentManager(BaseComponentManager):
+    """
+    A component manager for Tango devices, supporting:
+
+    * Maintaining a connection to its component
+
+    * Controlling its component via commands like Off(), Standby(),
+      On(), etc.
+
+    * Monitoring its component, e.g. detect that it has been turned off
+      or on
+
+    The current implementation is intended to
+
+    * illustrate the model
+    
+    * enable testing of these base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a component manager specific to the component managed by the device.
+    """
+
+    class _Component:
+        """
+        An example component for the component manager to work with.
+
+        It can be directly controlled via off(), standby(), on() and
+        reset() command methods.
+
+        For testing purposes, it can also be told to simulate a
+        spontaneous state change via simulate_off(), simulate_standby(),
+        simulate_on() and simulate_fault() methods.
+
+        When a component changes power mode, it lets the component
+        manager know by calling its ``component_off``,
+        ``component_standby`` and ``component_on`` callback methods.
+
+        When a component starts simulating a fault, it lets the
+        component manager know by calling its ``component_fault``
+        callback method.
+        """
+
+        def __init__(self, _power_mode=PowerMode.OFF, _faulty=False):
+            """
+            Initialise a new instance
+
+            :param _power_mode: initial power mode of this component
+                (for testing only)
+            :param _faulty: whether this component should initially
+                simulate a fault (for testing only)
+            """
+            self._power_mode = _power_mode
+            self._power_callback = None
+
+            self._faulty = _faulty
+            self._fault_callback = None
+
+        def set_op_callbacks(self, power_mode_callback, fault_callback):
+            """
+            Set callbacks for the underlying component
+
+            :param power_mode_callback: a callback to call when the
+                power mode of the component changes
+            :param fault_callback: a callback to call when the component
+                experiences a fault
+            """
+            self._power_callback = power_mode_callback
+            self._fault_callback = fault_callback
+
+        @property
+        def faulty(self):
+            """
+            Whether this component is currently experiencing a fault
+
+            :return: whether this component is faulting
+            :rtype: bool
+            """
+            return self._faulty
+
+        @property
+        def power_mode(self):
+            """
+            Current power mode of the component
+
+            :return: power mode of the component
+            :rtype: :py:class:`ska_tango_base.control_model.PowerMode`
+            """
+            if self.faulty:
+                raise ComponentFault()
+            return self._power_mode
+
+        def off(self):
+            """
+            Turn the component off
+            """
+            self.simulate_off()
+
+        def standby(self):
+            """
+            Put the component into low-power standby mode
+            """
+            self.simulate_standby()
+
+        def on(self):
+            """
+            Turn the component on
+            """
+            self.simulate_on()
+
+        def reset(self):
+            """
+            Reset the component (from fault state)
+            """
+            self._update_faulty(False)
+
+        def simulate_off(self):
+            """
+            Simulate the component being turned off, either
+            spontaneously or as a result of the Off command.
+            """
+            if self.faulty:
+                raise ComponentFault()
+            self._update_power_mode(PowerMode.OFF)
+
+        def simulate_standby(self):
+            """
+            Simulate the component going into low-power standby mode,
+            either spontaneously or as a result of the Standby command.
+            """
+            if self.faulty:
+                raise ComponentFault()
+            self._update_power_mode(PowerMode.STANDBY)
+
+        def simulate_on(self):
+            """
+            Simulate the component being turned on, either spontaneously
+            or as a result of the On command.
+            """
+            if self.faulty:
+                raise ComponentFault()
+            self._update_power_mode(PowerMode.ON)
+
+        def _invoke_power_callback(self):
+            """
+            Helper method that invokes the callback when the power mode
+            of the component changes.
+            """
+            if not self.faulty:
+                if self._power_callback is not None:
+                    self._power_callback(self._power_mode)
+
+        def _update_power_mode(self, power_mode):
+            """
+            Helper method that updates the power mode of the component,
+            ensuring that callbacks are called as
+            required.
+
+            :param power_mode: new value for the power mode of the
+                component
+            :type power_mode:
+                :py:class:`ska_tango_base.control_model.PowerMode`
+            """
+            if self._power_mode != power_mode:
+                self._power_mode = power_mode
+                self._invoke_power_callback()
+
+        def simulate_fault(self):
+            """
+            Tell the component to simulate a fault
+            """
+            self._update_faulty(True)
+
+        def _invoke_fault_callback(self):
+            """
+            Helper method that invokes the callback when the component
+            experiences a fault.
+            """
+            if self.faulty and self._fault_callback is not None:
+                self._fault_callback()
+
+        def _update_faulty(self, faulty):
+            """
+            Helper method that updates whether the component is
+            faulting or not, ensuring that callbacks are called as
+            required.
+
+            :param fault: new value for whether the component is
+                faulting or not
+            :type faulting: bool
+            """
+            if self._faulty != faulty:
+                self._faulty = faulty
+                self._invoke_fault_callback()
+
+    def __init__(self, op_state_model, *args, logger=None, _component=None, **kwargs):
+        """
+        Initialise a new ComponentManager instance
+
+        :param op_state_model: the op state model used by this component
+            manager
+        :param logger: a logger for this component manager
+        :param _component: allows setting of the component to be
+            managed; for testing purposes only
+        """
+        self.logger = logger
+
+        self._connected = False
+        self._fail_communicate = False
+
+        self._component = _component or self._Component()
+
+        super().__init__(op_state_model, *args, **kwargs)
+
+    def start_communicating(self):
+        """
+        Establish communication with the component, then start
+        monitoring.
+        """
+        if self._connected:
+            return
+
+        # trigger transition to UNKNOWN (e.g. from DISABLE) first,
+        # before trying to connect, because connection might take a
+        # little while, and we want to be in UNKNOWN state meanwhile.
+        self.op_state_model.perform_action("component_unknown")
+
+        # Now connect to the component. Here we simply consult the
+        # _fail_communicate attribute and either pretend to fail or pretend
+        # to succeed.
+        if self._fail_communicate:
+            raise ConnectionError("Failed to connect")
+
+        self._connected = True
+        self._component.set_op_callbacks(
+            self.component_power_mode_changed, self.component_fault
+        )
+        # we've been disconnected and we might have missed some
+        # changes, so we need to check the component's state, and
+        # make our state model correspond
+        if self._component.faulty:
+            self.component_fault()
+        else:
+            self.component_power_mode_changed(self._component.power_mode)
+
+    def stop_communicating(self):
+        """
+        Cease monitoring the component, and break off all communication
+        with it.
+        """
+        if not self._connected:
+            return
+
+        self._connected = False
+        self._component.set_op_callbacks(None, None)
+        self.op_state_model.perform_action("component_disconnected")
+
+    @property
+    def is_communicating(self):
+        """
+        Whether there is currently a connection to the component
+
+        :return: whether there is currently a connection to the
+            component
+        :rtype: bool
+        """
+        return self._connected
+
+    def simulate_communication_failure(self, fail_communicate):
+        """
+        Simulate (or stop simulating) a failure to communicate with the
+        component
+
+        :param fail_communicate: whether the connection to the component
+            is failing
+        """
+        self._fail_communicate = fail_communicate
+        if fail_communicate and self._connected:
+            self._connected = False
+            self._component.set_op_callbacks(None, None)
+            self.op_state_model.perform_action("component_unknown")
+
+    @property
+    @check_communicating
+    def power_mode(self):
+        """
+        Power mode of the component
+
+        :return: the power mode of the component
+        """
+        return self._component.power_mode
+
+    @property
+    @check_communicating
+    def faulty(self):
+        """
+        Whether the component is currently faulting
+
+        :return: whether the component is faulting
+        """
+        return self._component.faulty
+
+    @check_communicating
+    def off(self):
+        """
+        Turn the component off
+        """
+        self.logger.info("Turning component off")
+        self._component.off()
+
+    @check_communicating
+    def standby(self):
+        """
+        Put the component into low-power standby mode
+        """
+        self.logger.info("Putting component into standby mode")
+        self._component.standby()
+
+    @check_communicating
+    def on(self):
+        """
+        Turn the component on
+        """
+        self.logger.info("Turning component on")
+        self._component.on()
+
+    @check_communicating
+    def reset(self):
+        """
+        Reset the component (from fault state)
+        """
+        self.logger.info("Resetting component")
+        self._component.reset()
+
+    action_map = {
+        PowerMode.OFF: "component_off",
+        PowerMode.STANDBY: "component_standby",
+        PowerMode.ON: "component_on",
+    }
+
+    def component_power_mode_changed(self, power_mode):
+        """
+        Callback hook, called when whether the component power mode
+        changes
+
+        :param power_mode: the new power mode of the component
+        :type power_mode:
+            :py:class:`ska_tango_base.control_model.PowerMode`
+        """
+        action = self.action_map[power_mode]
+        self.op_state_model.perform_action(action)
+
+    def component_fault(self):
+        """
+        Callback hook, called when the component faults
+        """
+        self.op_state_model.perform_action("component_fault")
diff --git a/src/ska_tango_base/capability_device.py b/src/ska_tango_base/capability_device.py
index 3ede5b049ff6244ec1a8f42a26b92a9e1be3c762..9a2e1a0c3ae064a39696f414ae7fc4a55ed430c7 100644
--- a/src/ska_tango_base/capability_device.py
+++ b/src/ska_tango_base/capability_device.py
@@ -33,7 +33,7 @@ class SKACapability(SKAObsDevice):
         super().init_command_objects()
         self.register_command_object(
             "ConfigureInstances", self.ConfigureInstancesCommand(
-                self, self.state_model, self.logger
+                self, self.op_state_model, self.logger
             )
         )
 
diff --git a/src/ska_tango_base/commands.py b/src/ska_tango_base/commands.py
index 9fa639b02f38fef94a722bc7ce780fd50bac87b3..3d4834c6c87ef6dd4284b665c2a65afc5c6a3eef 100644
--- a/src/ska_tango_base/commands.py
+++ b/src/ska_tango_base/commands.py
@@ -1,10 +1,71 @@
 """
 This module provides abstract base classes for device commands, and a
 ResultCode enum.
+
+The following command classes are provided:
+
+* **BaseCommand**: that implements the common pattern for commands;
+  implement the do() method, and invoke the command class by *calling*
+  it.
+
+* **StateModelCommand**: implements a command that drives a state model.
+  For example, a command that drives the operational state of the
+  device, such as ``On()``, ``Standby()`` and ``Off()``, is a
+  ``StateModelCommand``.
+
+* **ObservationCommand**: implements a command that drives the
+  observation state of an obsDevice, such as a subarray; for example,
+  ``AssignResources()``, ``Configure()``, ``Scan()``.
+
+* **ResponseCommand**: for commands that return a ``(ResultCode,
+  message)`` tuple.
+  
+* **CompletionCommand**: for commands that need to let their state
+  machine know when they have completed; that is, long-running commands
+  with transitional states, such as ``AssignResources()`` and
+  ``Configure()``.
+
+.. inheritance-diagram::
+   ska_tango_base.commands.BaseCommand
+   ska_tango_base.commands.StateModelCommand
+   ska_tango_base.commands.ResponseCommand
+   ska_tango_base.commands.CompletionCommand
+   ska_tango_base.commands.ObservationCommand
+   :parts: 1
+
+Multiple inheritance is supported, and it is expected that many commands
+will need to inherit from more than one command class. For example, a
+subarray's ``AssignResources`` command would inherit from:
+
+* ``ObservationState``, because it drives observation state
+
+* ``ResponseCommand``, because it returns a `(ResultCode, message)`
+
+* ``CompletionCommand``, because it needs to let its state machine know
+  when it is completed.
+
+To use these commands: subclass from the command classes needed, then
+implement the ``__init__`` and ``do`` methods. For example:
+
+.. code-block:: py
+
+    class AssignResourcesCommand(
+        ObservationCommand, ResponseCommand, CompletionCommand
+    ):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
+            super().__init__(target, obs_state_model, "assign", op_state_model, logger=logger)
+
+        def do(self, argin):
+            # do stuff
+            return (ResultCode.OK, "AssignResources command completed OK")
+
 """
 import enum
 import logging
-from ska_tango_base.faults import CommandError, ResultCodeError, StateModelError
+
+from tango import DevState
+
+from ska_tango_base.faults import CommandError, StateModelError
 
 module_logger = logging.getLogger(__name__)
 
@@ -42,21 +103,17 @@ class ResultCode(enum.IntEnum):
 
 class BaseCommand:
     """
-    Abstract base class for Tango device server commands. Ensures the
-    command is run, and that if the command errors, the "fatal_error"
-    action will be called on the state model.
+    Abstract base class for Tango device server commands. Checks that
+    the command is allowed to run in the current state, and runs the
+    command.
     """
 
-    def __init__(self, target, state_model, logger=None):
+    def __init__(self, target, *args, logger=None, **kwargs):
         """
         Creates a new BaseCommand object for a device.
 
-        :param state_model: the state model that this command uses, for
-             example to raise a fatal error if the command errors out.
-        :type state_model: SKABaseClassStateModel or a subclass of same
-        :param target: the object that this base command acts upon. For
-            example, the device that this BaseCommand implements the
-            command for.
+        :param target: the object that this command acts upon; for
+            example, a component manager
         :type target: object
         :param logger: the logger to be used by this Command. If not
             provided, then a default module logger will be used.
@@ -65,7 +122,6 @@ class BaseCommand:
         """
         self.name = self.__class__.__name__
         self.target = target
-        self.state_model = state_model
         self.logger = logger or module_logger
 
     def __call__(self, argin=None):
@@ -84,7 +140,6 @@ class BaseCommand:
             self.logger.exception(
                 f"Error executing command {self.name} with argin '{argin}'"
             )
-            self.fatal_error()
             raise
 
     def _call_do(self, argin=None):
@@ -120,52 +175,159 @@ class BaseCommand:
             "BaseCommand is abstract; do() must be subclassed not called."
         )
 
-    def fatal_error(self):
+
+class StateModelCommand(BaseCommand):
+    def __init__(self, target, state_model, action_slug, *args, logger=None, **kwargs):
         """
-        Callback for a fatal error in the command, such as an unhandled
-        exception.
+        A base command for commands that drive a state model.
+
+        :param target: the object that this command acts upon; for
+            example, a component manager
+        :type target: object
+        :param state_model: the state model that this command uses, for
+            example to raise a fatal error if the command errors out.
+        :type state_model: a state model, such as an OperationStateModel
+        :param action_slug: a slug for this command, used to construct
+            actions on the state model corresponding to this command.
+            For example, if we set the slug for the Scan() command to
+            "scan", then invoking the command would correspond to the
+            "scan_invoked" action on the state model. This can be set to
+            None, in which case no action is taken.
+        :param args: additional positional arguments
+        :param logger: the logger to be used by this Command. If not
+            provided, then a default module logger will be used.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param kwargs: additional keyword arguments
         """
-        self._perform_action("fatal_error")
+        self.state_model = state_model
+        self._action_slug = action_slug
+
+        if self._action_slug is None:
+            self._invoked_action = None
+        else:
+            self._invoked_action = f"{action_slug}_invoked"
+
+        super().__init__(target, *args, logger=logger, **kwargs)
 
-    def _is_action_allowed(self, action):
+    def __call__(self, argin=None):
         """
-        Helper method; whether a given action is permitted in the
-        current state of the state model.
+        What to do when the command is called. Ensures that we perform
+        the "invoked" action on the state machine.
 
-        :param action: the action on the state model that is being
-            scrutinised
-        :type action: string
-        :returns: whether the action is allowed
-        :rtype: boolean
+        :param argin: the argument passed to the Tango command, if
+            present
+        :type argin: ANY
+
+        :return: result of call
+
+        :raises CommandError: if the command is not allowed
         """
-        return self.state_model.is_action_allowed(action)
+        if self._invoked_action is not None:
+            try:
+                self.state_model.perform_action(self._invoked_action)
+            except StateModelError as sme:
+                raise CommandError("Command not permitted by state model.") from sme
+
+        return super().__call__(argin)
 
-    def _try_action(self, action):
+    def is_allowed(self, raise_if_disallowed=False):
         """
-        Helper method; "tries" an action on the state model.
+        Whether this command is allowed to run in the current state of
+        the state model.
+
+        :param raise_if_disallowed: whether to raise an error or
+            simply return False if the command is disallowed
+
+        :returns: whether this command is allowed to run
+        :rtype: boolean
 
-        :param action: the action to perform on the state model
-        :type action: string
-        :raises CommandError: if the action is not allowed in current state
-        :returns: True is the action is allowed
+        :raises CommandError: if the command is not allowed and
+            `raise_if_disallowed` is True
         """
+        if self._invoked_action is None:
+            return True
+
         try:
-            return self.state_model.try_action(action)
-        except StateModelError as exc:
+            return self.state_model.is_action_allowed(
+                self._invoked_action,
+                raise_if_disallowed=raise_if_disallowed
+            )
+        except StateModelError as state_model_error:
             raise CommandError(
                 f"Error executing command {self.name}"
-            ) from exc
+            ) from state_model_error
+
 
-    def _perform_action(self, action):
+class ObservationCommand(StateModelCommand):
+    def __init__(
+        self,
+        target,
+        obs_state_model,
+        action_slug,
+        op_state_model,
+        *args,
+        logger=None,
+        **kwargs
+    ):
         """
-        Helper method; performs an action on the state model, thus
-        driving state
+        A base class for commands that drive the device's observing
+        state. This is a special case of a ``StateModelCommand`` because
+        although it only drives the observation state model, it has to
+        check also the operational state model to determine whether it
+        is allowed to run.
 
-        :param action: the action to perform on the state model
-        :type action: string
+        :param target: the object that this command acts upon; for
+            example, a component manager
+        :type target: object
+        :param obs_state_model: the observation state model that
+                this command uses to check that it is allowed to run,
+                and that it drives with actions.
+        :type obs_state_model: :py:class:`ObsStateModel`
+        :param action_slug: a slug for this command, used to construct
+            actions on the state model corresponding to this command.
+            For example, if we set the slug for the Scan() command to
+            "scan", then invoking the command would correspond to the
+            "scan_invoked" action on the state model.
+        :param op_state_model: the op state model that this command
+            uses to check that it is allowed to run
+        :type op_state_model: :py:class:`OpStateModel`
+        :param logger: the logger to be used by this Command. If not
+            provided, then a default module logger will be used.
+        :type logger: a logger that implements the standard library
+            logger interface
         """
-        self.state_model.perform_action(action)
+        self._op_state_model = op_state_model
+        super().__init__(
+            target, obs_state_model, action_slug, *args, logger=logger, **kwargs
+        )
+        self._action_slug = action_slug
+        self._invoked_action = f"{action_slug}_invoked"
 
+    def is_allowed(self, raise_if_disallowed=False):
+        """
+        Whether this command is allowed to run in the current state of
+        the state model.
+
+        :param raise_if_disallowed: whether to raise an error or
+            simply return False if the command is disallowed
+
+        :returns: whether this command is allowed to run
+        :rtype: boolean
+
+        :raises CommandError: if the command is not allowed and
+            `raise_if_disallowed` is True
+        """
+        if self._op_state_model.op_state != DevState.ON:
+            if raise_if_disallowed:
+                raise CommandError(
+                    "Observation commands are only permitted in Op state ON."
+                )
+            else:
+                return False
+
+        return super().is_allowed(raise_if_disallowed=raise_if_disallowed)
+    
 
 class ResponseCommand(BaseCommand):
     """
@@ -182,26 +344,6 @@ class ResponseCommand(BaseCommand):
         ResultCode.UNKNOWN: logging.WARNING
     }
 
-    def __call__(self, argin=None):
-        """
-        What to do when the command is called. This base class simply
-        calls ``do()`` or ``do(argin)``, depending on whether the
-        ``argin`` argument is provided.
-
-        :param argin: the argument passed to the Tango command, if
-            present
-        :type argin: ANY
-        """
-        try:
-            (return_code, message) = self._call_do(argin)
-        except Exception:
-            self.logger.exception(
-                f"Error executing command {self.name} with argin '{argin}'"
-            )
-            self.fatal_error()
-            raise
-        return (return_code, message)
-
     def _call_do(self, argin=None):
         """
         Helper method that ensures the ``do`` method is called with the
@@ -210,6 +352,11 @@ class ResponseCommand(BaseCommand):
         :param argin: the argument passed to the Tango command, if
             present
         :type argin: ANY
+
+        :return: A tuple containing a return code and a string
+            message indicating status. The message is for
+            information purpose only.
+        :rtype: (ResultCode, str)
         """
         if argin is None:
             (return_code, message) = self.do()
@@ -218,52 +365,47 @@ class ResponseCommand(BaseCommand):
 
         self.logger.log(
             self.RESULT_LOG_LEVEL.get(return_code, logging.ERROR),
-            f"Exiting command {self.name} with return_code {return_code!s}, "
-            f"message: '{message}'"
+            f"Exiting command {self.name} with return_code "
+            f"{return_code!s}, message: '{message}'."
         )
         return (return_code, message)
 
 
-class ActionCommand(ResponseCommand):
+class CompletionCommand(StateModelCommand):
     """
-    Abstract base class for a tango command, which checks a state model
-    to find out whether the command is allowed to be run, and after
-    running, sends an action to that state model, thus driving device
-    state.
+    Abstract base class for a command that sends a "completed" action to
+    the state model at command completion.
     """
 
     def __init__(
-        self, target, state_model, action_hook, start_action=False, logger=None
+        self, target, state_model, action_slug, *args, logger=None, **kwargs
     ):
         """
-        Create a new ActionCommand for a device.
+        Create a new CompletionCommand for a device.
 
-        :param target: the object that this base command acts upon. For
-            example, the device that this ActionCommand implements the
-            command for.
+        :param target: the object that this command acts upon; for
+            example, a component manager
         :type target: object
-        :param action_hook: a hook for the command, used to build
-            actions that will be sent to the state model; for example,
-            if the hook is "scan", then success of the command will
-            result in action "scan_succeeded" being sent to the state
-            model.
-        :type action_hook: string
-        :param start_action: whether the state model supports a start
-            action (i.e. to put the state model into an transient state
-            while the command is running); default False
-        :type start_action: boolean
+        :param state_model: the state model that this command uses, for
+            example to raise a fatal error if the command errors out.
+        :type state_model: a state model, such as
+            `OperationalStateModel`
+        :param action_slug: a slug for this command, used to construct
+            actions on the state model corresponding to this command.
+            For example, if we set the slug for the Scan() command to
+            "scan", then invokation and completion of the command would
+            correspond respectively to the "scan_invoked" and
+            "scan_completed" actions on the state model.
+        :type action_slug: string
+        :param args: additional positional arguments
         :param logger: the logger to be used by this Command. If not
             provided, then a default module logger will be used.
         :type logger: a logger that implements the standard library
             logger interface
+        :param kwargs: additional keyword arguments
         """
-        super().__init__(target, state_model, logger=logger)
-        self._succeeded_hook = f"{action_hook}_succeeded"
-        self._failed_hook = f"{action_hook}_failed"
-
-        self._started_hook = None
-        if start_action:
-            self._started_hook = f"{action_hook}_started"
+        super().__init__(target, state_model, action_slug, *args, logger=logger, **kwargs)
+        self._completed_hook = f"{action_slug}_completed"
 
     def __call__(self, argin=None):
         """
@@ -275,79 +417,15 @@ class ActionCommand(ResponseCommand):
         :param argin: the argument passed to the Tango command, if
             present
         :type argin: ANY
-        """
-        self.check_allowed()
-        try:
-            self.started()
-            (return_code, message) = self._call_do(argin)
-            self._returned(return_code)
-        except Exception:
-            self.logger.exception(
-                f"Error executing command {self.name} with argin '{argin}'"
-            )
-            self.fatal_error()
-            raise
-        return (return_code, message)
 
-    def _returned(self, return_code):
-        """
-        Helper method that handles the return of the ``do()`` method.
-        If the return code is OK or FAILED, then it performs an
-        appropriate action on the state model. Otherwise it raises an
-        error.
-
-        :param return_code: The return_code returned by the ``do()``
-            method
-        :type return_code: :py:class:`ResultCode`
-        """
-        if return_code == ResultCode.OK:
-            self.succeeded()
-        elif return_code == ResultCode.FAILED:
-            self.failed()
-        else:
-            if self._started_hook is None:
-                raise ResultCodeError(
-                    f"ActionCommands that do not have a started action may"
-                    f"only return with code OK or FAILED, not {return_code!s}."
-                )
-
-    def check_allowed(self):
-        """
-        Checks whether the command is allowed to be run in the current
-        state of the state model.
-
-        :returns: True if the command is allowed to be run
-        :raises StateModelError: if the command is not allowed to be run
-        """
-        return self._try_action(self._started_hook or self._succeeded_hook)
-
-    def is_allowed(self):
-        """
-        Whether this command is allowed to run in the current state of
-        the state model.
-
-        :returns: whether this command is allowed to run
-        :rtype: boolean
-        """
-        return self._is_action_allowed(
-            self._started_hook or self._succeeded_hook
-        )
-
-    def started(self):
-        """
-        Action to perform upon starting the comand.
-        """
-        if self._started_hook is not None:
-            self._perform_action(self._started_hook)
-
-    def succeeded(self):
-        """
-        Callback for the successful completion of the command.
+        :return: The result of the call.
         """
-        self._perform_action(self._succeeded_hook)
+        result = super().__call__(argin)
+        self.completed()
+        return result
 
-    def failed(self):
+    def completed(self):
         """
-        Callback for the failed completion of the command.
+        Callback for the completion of the command.
         """
-        self._perform_action(self._failed_hook)
+        self.state_model.perform_action(self._completed_hook)
diff --git a/src/ska_tango_base/control_model.py b/src/ska_tango_base/control_model.py
index cfc5075708d6d1f7c54c9a1239a57994ecd19905..032909062ff423f8e607b70e96562556457b221c 100644
--- a/src/ska_tango_base/control_model.py
+++ b/src/ska_tango_base/control_model.py
@@ -24,7 +24,7 @@ class HealthState(enum.IntEnum):
 
     OK = 0
     """
-    TANGO Device reports this state when ready for use, or when entity ``adminMode``
+    Tango Device reports this state when ready for use, or when entity ``adminMode``
     is ``NOT_FITTED`` or ``RESERVED``.
 
     The rationale for reporting health as OK when an entity is ``NOT_FITTED`` or
@@ -35,7 +35,7 @@ class HealthState(enum.IntEnum):
 
     DEGRADED = 1
     """
-    TANGO Device reports this state when only part of functionality is available. This
+    Tango Device reports this state when only part of functionality is available. This
     value is optional and shall be implemented only where it is useful.
 
     For example, a subarray may report healthState as ``DEGRADED`` if one of the dishes
@@ -45,12 +45,12 @@ class HealthState(enum.IntEnum):
     (quantified) and documented. For example, the difference between ``DEGRADED`` and ``FAILED``
     subarray can be defined as the number or percent of the dishes available, the number or
     percent of the baselines available,   sensitivity, or some other criterion. More than one
-    criteria may be defined for a TANGO Device.
+    criteria may be defined for a Tango Device.
     """
 
     FAILED = 2
     """
-    TANGO Device reports this state when unable to perform core functionality and
+    Tango Device reports this state when unable to perform core functionality and
     produce valid output.
     """
 
@@ -65,61 +65,54 @@ class AdminMode(enum.IntEnum):
 
     ONLINE = 0
     """
-    SKA operations declared that the entity can be used for observing (or other
-    function it implements). During normal operations Elements and subarrays
-    (and all other entities) shall be in this mode. TANGO Devices that implement
-    ``adminMode`` as read-only attribute shall always report ``adminMode=ONLINE``.
-    ``adminMode=ONLINE`` is also used to indicate active Subarrays or Capabilities.
+    The component can be used for normal operations, such as observing.
+    While in this mode, the Tango device is actively monitoring and
+    controlling its component. Tango devices that implement
+    ``adminMode`` as a read-only attribute shall always report
+    ``adminMode=ONLINE``.
     """
 
     OFFLINE = 1
-    """SKA operations declared that the entity is not used for observing or other function it
-    provides. A subset of the monitor and control functionality may be supported in this mode.
-    ``adminMode=OFFLINE`` is also used to indicate unused Subarrays and unused Capabilities.
-    TANGO devices report ``state=DISABLED`` when ``adminMode=OFFLINE``.
+    """
+    The component is not to be used for any operations. While in this
+    mode, Tango devices report ``state=DISABLE``, and do not communicate
+    with their component. Monitoring and control of the component does
+    not occur, so alarms, alerts and events are not received.
     """
 
     MAINTENANCE = 2
     """
-    SKA operations declared that the entity is reserved for maintenance and cannot
-    be part of scientific observations, but can be used for observing in a ‘Maintenance Subarray’.
-
-    ``MAINTENANCE`` mode has different meaning for different entities, depending on the context
-    and functionality. Some entities may implement different behaviour when in ``MAINTENANCE``
-    mode.
+    SKA operations declares that the component cannot be used for normal
+    operations, but can be used for maintenance purposes such as testing
+    and debugging, as part of a "maintenance subarray". While in this
+    mode, Tango devices are actively monitoring and controlling their
+    component, but may only support a subset of normal functionality.
 
-    For each TANGO Device, the difference in behaviour and functionality in ``MAINTENANCE`` mode
-    shall be documented. ``MAINTENANCE`` is the factory default for ``adminMode``. Transition
-    out of ``adminMode=NOT_FITTED`` is always via ``MAINTENANCE``; an engineer/operator has to
-    verify that the  entity is operational as expected before it is set to ``ONLINE``
-    (or ``OFFLINE``).
+    ``MAINTENANCE`` mode has different meaning for different components,
+    depending on the context and functionality. Some entities may
+    implement different behaviour when in ``MAINTENANCE`` mode. For each
+    Tango device, the difference in behaviour and functionality in
+    ``MAINTENANCE`` mode shall be documented.
     """
 
     NOT_FITTED = 3
     """
-    SKA operations declared the entity as ``NOT_FITTED`` (and therefore cannot be used for
-    observing or other function it provides). TM shall not send commands or queries to the
-    Element (entity) while in this mode.
-
-    TANGO devices shall report ``state=DISABLE`` when ``adminMode=NOT_FITTED``; higher level
-    entities (Element, Sub-element, component, Subarray and/or Capability) which ‘use’
-    ``NOT_FITTED`` equipment shall report operational ``state`` as ``DISABLE``.  If only a subset
-    of higher-level functionality is affected, overall ``state`` of the higher-level entity that
-    uses ``NOT_FITTED`` equipment may be reported as ``ON``, but with ``healthState=DEGRADED``.
-    Additional queries may be necessary to identify which functionality and capabilities are
-    available.
-
-    Higher-level entities shall intelligently exclude ``NOT_FITTED`` items from ``healthState`` and
-    Element Alerts/Telescope Alarms; e.g. if a receiver band in DSH is ``NOT_FITTED`` and there
-    is no communication to that receiver band, then DSH shall not raise Element Alerts for that
-    entity and it should not report ``healthState=FAILED`` because of an entity that is
-    ``NOT_FITTED``.
+    The component cannot be used for any purposes because it is not
+    fitted; for example, faulty equipment has been removed and not
+    yet replaced, leaving nothing `in situ` to monitor. While in this
+    mode, Tango devices report ``state=DISABLED``. All monitoring and
+    control functionality is disabled because there is no component to
+    monitor.
     """
 
     RESERVED = 4
-    """This mode is used to identify additional equipment that is ready to take over when the
-    operational equipment fails. This equipment does not take part in the operations at this
-    point in time. TANGO devices report ``state=DISABLED`` when ``adminMode=RESERVED``.
+    """
+    The component is fitted, but only for redundancy purposes. It is
+    additional equipment that does not take part in operations at this
+    time, but is ready to take over when the operational
+    equipment fails. While in this mode, Tango devices report
+    ``state=DISABLED``. All monitoring and control functionality is
+    disabled.
     """
 
 
@@ -258,12 +251,12 @@ class ControlMode(enum.IntEnum):
 
     REMOTE = 0
     """
-    TANGO Device accepts commands from all clients.
+    Tango Device accepts commands from all clients.
     """
 
     LOCAL = 1
     """
-    TANGO Device accepts only from a ‘local’ client and ignores commands and queries received
+    Tango Device accepts only from a ‘local’ client and ignores commands and queries received
     from TM or any other ‘remote’ clients. This is typically activated by a switch,
     or a connection on the local control interface. The intention is to support early
     integration of DISHes and stations. The equipment has to be put back in ``REMOTE``
@@ -326,3 +319,14 @@ class LoggingLevel(enum.IntEnum):
     WARNING = 3
     INFO = 4
     DEBUG = 5
+
+
+class PowerMode(enum.IntEnum):
+    """
+    Enumerated type for the power mode of components that rely upon a
+    power supply, such as hardware.
+    """
+    UNKNOWN = 0
+    OFF = 1
+    STANDBY = 2
+    ON = 3
diff --git a/src/ska_tango_base/csp/__init__.py b/src/ska_tango_base/csp/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae357772a6b8fa25736b7c00ddfa3359a89af02a
--- /dev/null
+++ b/src/ska_tango_base/csp/__init__.py
@@ -0,0 +1,29 @@
+"""
+This subpackage contains base devices specific to CSP.
+"""
+
+__all__ = (
+    "CspSubElementObsStateModel",
+    "CspObsComponentManager",
+    "CspSubarrayComponentManager",
+    "ReferenceCspObsComponentManager",
+    "ReferenceCspSubarrayComponentManager",
+    "CspSubElementMaster",
+    "CspSubElementObsDevice",
+    "CspSubElementSubarray",
+)
+
+from .master_device import CspSubElementMaster
+
+from .obs import (
+    CspSubElementObsStateModel,
+    CspObsComponentManager,
+    ReferenceCspObsComponentManager,
+    CspSubElementObsDevice,
+)
+
+from .subarray import (
+    CspSubarrayComponentManager,
+    ReferenceCspSubarrayComponentManager,
+    CspSubElementSubarray
+)
diff --git a/src/ska_tango_base/csp_subelement_master.py b/src/ska_tango_base/csp/master_device.py
similarity index 82%
rename from src/ska_tango_base/csp_subelement_master.py
rename to src/ska_tango_base/csp/master_device.py
index 7bd30eb57515bdb25bad1c03b48d316f5f71ae13..c066c7da285a54c45dcefb655902d3a14e426d11 100644
--- a/src/ska_tango_base/csp_subelement_master.py
+++ b/src/ska_tango_base/csp/master_device.py
@@ -22,7 +22,7 @@ from tango.server import run, attribute, command, device_property
 # SKA specific imports
 
 from ska_tango_base import SKAMaster
-from ska_tango_base.commands import ResultCode, ResponseCommand
+from ska_tango_base.commands import ResultCode, ResponseCommand, StateModelCommand
 from ska_tango_base.control_model import AdminMode
 from ska_tango_base.faults import CommandError
 # PROTECTED REGION END #    //  CspSubElementMaster.additionnal_import
@@ -204,10 +204,13 @@ class CspSubElementMaster(SKAMaster):
         Sets up the command objects
         """
         super().init_command_objects()
-        device_args = (self, self.state_model, self.logger)
         self.register_command_object(
-            "LoadFirmware", self.LoadFirmwareCommand(*device_args)
+            "LoadFirmware",
+            self.LoadFirmwareCommand(
+                self, self.op_state_model, self.admin_mode_model, self.logger
+            )
         )
+        device_args = (self, self.op_state_model, self.logger)
         self.register_command_object(
             "PowerOnDevices", self.PowerOnDevicesCommand(*device_args)
         )
@@ -265,7 +268,7 @@ class CspSubElementMaster(SKAMaster):
             return (ResultCode.OK, message)
 
     def always_executed_hook(self):
-        """Method always executed before any TANGO command is executed."""
+        """Method always executed before any Tango command is executed."""
         # PROTECTED REGION ID(CspSubElementMaster.always_executed_hook) ENABLED START #
         # PROTECTED REGION END #    //  CspSubElementMaster.always_executed_hook
 
@@ -412,15 +415,39 @@ class CspSubElementMaster(SKAMaster):
     # --------
     # Commands
     # --------
-    class LoadFirmwareCommand(ResponseCommand):
+    class LoadFirmwareCommand(StateModelCommand, ResponseCommand):
         """
-        A class for the CspSubElementMaster's LoadFirmware command.
+        A class for the LoadFirmware command.
         """
+        def __init__(self, target, op_state_model, admin_mode_model, *args, logger=None, **kwargs):
+            """
+            Creates a new BaseCommand object for a device.
+
+            :param target: the object that this base command acts upon. For
+                example, the device's component manager.
+            :type target: object
+            :param op_state_model: the op state model that this command
+                uses.
+            :type op_state_model: OpStateModel
+            :param admin_mode_model: the admin model that this command
+                uses.
+            :type admin_mode_model: AdminModeModel
+            :param args: other positional arguments
+            :param kwargs: other keyword arguments
+            :param logger: the logger to be used by this Command. If not
+                provided, then a default module logger will be used.
+            :type logger: a logger that implements the standard library
+                logger interface
+            """
+            self._admin_mode_model = admin_mode_model
+            super().__init__(target, op_state_model, None, *args, logger=logger, **kwargs)
 
         def do(self, argin):
             """
             Stateless hook for device LoadFirmware() command.
 
+            :param argin: argument to command, currently unused
+
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
@@ -429,34 +456,45 @@ class CspSubElementMaster(SKAMaster):
             message = "LoadFirmware command completed OK"
             return (ResultCode.OK, message)
 
-        def check_allowed(self):
+        def is_allowed(self, raise_if_disallowed=False):
             """
             Check if the command is in the proper state (State/adminMode)
             to be executed.
             The master device has to be in OFF/MAINTENACE to process the
             LoadFirmware command.
 
-            :raises: ``CommandError`` if command not allowed
+            :param raise_if_disallowed: whether to raise an error or
+                simply return False if the command is disallowed
+
+            :raises CommandError: if command not allowed
             :return: ``True`` if the command is allowed.
             :rtype: boolean
             """
-            if (self.state_model.op_state == tango.DevState.OFF
-                    and self.state_model.admin_mode == AdminMode.MAINTENANCE):
+            allowed = (self.state_model.op_state == tango.DevState.OFF and
+                       self._admin_mode_model.admin_mode == AdminMode.MAINTENANCE)
+            if allowed:
                 return True
-            msg = "{} not allowed in {}/{}".format(self.name,
-                                                   self.state_model.op_state,
-                                                   AdminMode(self.state_model.admin_mode).name)
-            raise CommandError(msg)
-
-    class PowerOnDevicesCommand(ResponseCommand):
+            if raise_if_disallowed:
+                raise CommandError(
+                    f"{self.name} not allowed in {self.state_model.op_state}"
+                    f"/{self._admin_mode_model.admin_mode.name}"
+                )
+            return False
+
+    class PowerOnDevicesCommand(StateModelCommand, ResponseCommand):
         """
         A class for the CspSubElementMaster's PowerOnDevices command.
         """
 
+        def __init__(self, target, op_state_model, *args, logger=None, **kwargs):
+            super().__init__(target, op_state_model, None, *args, logger=logger, **kwargs)
+
         def do(self, argin):
             """
             Stateless hook for device PowerOnDevices() command.
 
+            :param argin: argument to command, currently unused
+
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
@@ -465,31 +503,41 @@ class CspSubElementMaster(SKAMaster):
             message = "PowerOnDevices command completed OK"
             return (ResultCode.OK, message)
 
-        def check_allowed(self):
+        def is_allowed(self, raise_if_disallowed=False):
             """
             Check if the command is in the proper state to be executed.
             The master device has to be in ON to process the
             PowerOnDevices command.
 
-            : raises: ``CommandError`` if command not allowed
-            : return: ``True`` if the command is allowed.
-            : rtype: boolean
+            :param raise_if_disallowed: whether to raise an error or
+                simply return False if the command is disallowed
+
+            :raises CommandError: if command not allowed
+            :return: ``True`` if the command is allowed.
+            :rtype: boolean
             """
             if self.state_model.op_state == tango.DevState.ON:
                 return True
-            msg = "{} not allowed in {}".format(self.name,
-                                                self.state_model.op_state)
-            raise CommandError(msg)
+            if raise_if_disallowed:
+                raise CommandError(
+                    f"{self.name} not allowed in {self.state_model.op_state}"
+                )
+            return False
 
-    class PowerOffDevicesCommand(ResponseCommand):
+    class PowerOffDevicesCommand(StateModelCommand, ResponseCommand):
         """
         A class for the CspSubElementMaster's PowerOffDevices command.
         """
 
+        def __init__(self, target, op_state_model, *args, logger=None, **kwargs):
+            super().__init__(target, op_state_model, None, *args, logger=logger, **kwargs)
+
         def do(self, argin):
             """
             Stateless hook for device PowerOffDevices() command.
 
+            :param argin: argument to command, currently unused
+
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
@@ -498,31 +546,41 @@ class CspSubElementMaster(SKAMaster):
             message = "PowerOffDevices command completed OK"
             return (ResultCode.OK, message)
 
-        def check_allowed(self):
+        def is_allowed(self, raise_if_disallowed=False):
             """
             Check if the command is in the proper state to be executed.
             The master device has to be in ON to process the
             PowerOffDevices command.
 
-            : raises: ``CommandError`` if command not allowed
-            : return: ``True`` if the command is allowed.
-            : rtype: boolean
+            :param raise_if_disallowed: whether to raise an error or
+                simply return False if the command is disallowed
+
+            :raises CommandError: if command not allowed
+            :return: ``True`` if the command is allowed.
+            :rtype: boolean
             """
             if self.state_model.op_state == tango.DevState.ON:
                 return True
-            msg = "{} not allowed in {}".format(self.name,
-                                                self.state_model.op_state)
-            raise CommandError(msg)
+            if raise_if_disallowed:
+                raise CommandError(
+                    f"{self.name} not allowed in {self.state_model.op_state}"
+                )
+            return False
 
-    class ReInitDevicesCommand(ResponseCommand):
+    class ReInitDevicesCommand(StateModelCommand, ResponseCommand):
         """
         A class for the CspSubElementMaster's ReInitDevices command.
         """
 
+        def __init__(self, target, op_state_model, *args, logger=None, **kwargs):
+            super().__init__(target, op_state_model, None, *args, logger=logger, **kwargs)
+
         def do(self, argin):
             """
             Stateless hook for device ReInitDevices() command.
 
+            :param argin: argument to command, currently unused
+
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
@@ -531,33 +589,37 @@ class CspSubElementMaster(SKAMaster):
             message = "ReInitDevices command completed OK"
             return (ResultCode.OK, message)
 
-        def check_allowed(self):
+        def is_allowed(self, raise_if_disallowed=False):
             """
             Check if the command is in the proper state to be executed.
             The master device has to be in ON to process the
             ReInitDevices command.
 
-            : raises: ``CommandError`` if command not allowed
-            : return: ``True`` if the command is allowed.
-            : rtype: boolean
+            :param raise_if_disallowed: whether to raise an error or
+                simply return False if the command is disallowed
+
+            :raises CommandError: if command not allowed
+            :return: ``True`` if the command is allowed.
+            :rtype: boolean
             """
             if self.state_model.op_state == tango.DevState.ON:
                 return True
-            msg = "{} not allowed in {}".format(self.name,
-                                                self.state_model.op_state)
-            raise CommandError(msg)
+            if raise_if_disallowed:
+                raise CommandError(
+                    f"{self.name} not allowed in {self.state_model.op_state}."
+                )
+            return True
 
     def is_LoadFirmware_allowed(self):
         """
         Check if the LoadFirmware command is allowed in the current
         state.
 
-        :raises: ``CommandError`` if command not allowed
         :return: ``True`` if command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("LoadFirmware")
-        return command.check_allowed()
+        return command.is_allowed(True)
 
     @command(
         dtype_in='DevVarStringArray',
@@ -596,12 +658,11 @@ class CspSubElementMaster(SKAMaster):
         Check if the PowerOnDevice command is allowed in the current
         state.
 
-        :raises ``tango.DevFailed`` if command not allowed
-        :return ``True`` if command is allowed
+        :return: ``True`` if command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("PowerOnDevices")
-        return command.check_allowed()
+        return command.is_allowed(True)
 
     @command(
         dtype_in='DevVarStringArray',
@@ -633,12 +694,11 @@ class CspSubElementMaster(SKAMaster):
         Check if the PowerOffDevices command is allowed in the current
         state.
 
-        :raises: ``tango.DevFailed`` if command not allowed
         :return: ``True`` if command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("PowerOffDevices")
-        return command.check_allowed()
+        return command.is_allowed(True)
 
     @command(
         dtype_in='DevVarStringArray',
@@ -671,12 +731,11 @@ class CspSubElementMaster(SKAMaster):
         Check if the ReInitDevices command is allowed in the current
         state.
 
-        :raises: ``tango.DevFailed`` if command not allowed
         :return: ``True`` if command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("ReInitDevices")
-        return command.check_allowed()
+        return command.is_allowed(True)
 
     @command(
         dtype_in='DevVarStringArray',
@@ -690,7 +749,7 @@ class CspSubElementMaster(SKAMaster):
         """
         Reinitialize the devices passed in the input argument.
         The exact functionality may vary for different devices
-        and sub-systems, each TANGO Device/Server should define
+        and sub-systems, each Tango Device/Server should define
         what does ReInitDevices means.
         Ex:
         ReInitDevices FPGA -> reset
@@ -716,7 +775,14 @@ class CspSubElementMaster(SKAMaster):
 
 
 def main(args=None, **kwargs):
-    """Main function of the CspSubElementMaster module."""
+    """
+    Entry point for the CspSubElementMaster module.
+
+    :param args: str
+    :param kwargs: str
+
+    :return: exit code
+    """
     # PROTECTED REGION ID(CspSubElementMaster.main) ENABLED START #
     return run((CspSubElementMaster,), args=args, **kwargs)
     # PROTECTED REGION END #    //  CspSubElementMaster.main
diff --git a/src/ska_tango_base/csp/obs/__init__.py b/src/ska_tango_base/csp/obs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fbb20c6ca506e010518c50e66aed085c8738855c
--- /dev/null
+++ b/src/ska_tango_base/csp/obs/__init__.py
@@ -0,0 +1,20 @@
+"""
+This subpackage contains obs device functionality specific to CSP.
+"""
+
+__all__ = (
+    "CspSubElementObsStateModel",
+    "CspObsComponentManager",
+    "ReferenceCspObsComponentManager",
+    "CspSubElementObsDevice",
+)
+
+# Note: order of imports is important - start with lowest in the hierarchy
+from .obs_state_model import CspSubElementObsStateModel
+
+from .component_manager import CspObsComponentManager
+
+from .reference_component_manager import ReferenceCspObsComponentManager
+
+from .obs_device import CspSubElementObsDevice
+
diff --git a/src/ska_tango_base/csp/obs/component_manager.py b/src/ska_tango_base/csp/obs/component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..d22598acdeb82f9094dbdc41d08d813f910620b5
--- /dev/null
+++ b/src/ska_tango_base/csp/obs/component_manager.py
@@ -0,0 +1,104 @@
+"""
+This module models component management for CSP subelement observation devices.
+"""
+from ska_tango_base.base import BaseComponentManager
+
+class CspObsComponentManager(BaseComponentManager):
+    """
+    A component manager for SKA CSP subelement observation Tango devices:
+
+    The current implementation is intended to
+    * illustrate the model
+    * enable testing of the base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a subclass specific to the component managed by the device.
+    """
+
+    def __init__(self, op_state_model, obs_state_model, *args, **kwargs):
+        self.obs_state_model = obs_state_model
+        super().__init__(op_state_model, *args, **kwargs)
+
+    def configure_scan(self, configuration):
+        """
+        Configure the component
+
+        :param configuration: the configuration to be configured
+        :type configuration: dict
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def deconfigure(self):
+        """
+        Deconfigure this component.
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def scan(self, args):
+        """
+        Start scanning
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def end_scan(self):
+        """
+        End scanning
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def abort(self):
+        """
+        Tell the component to abort whatever it was doing
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def obsreset(self):
+        """
+        Tell the component to reset to an unconfigured state (but
+        without releasing any assigned resources)
+        """
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    @property
+    def config_id(self):
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    @property
+    def scan_id(self):
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    @config_id.setter
+    def config_id(self, config_id):
+        raise NotImplementedError("CspObsComponentManager is abstract.")
+
+    def component_configured(self, configured):
+        """
+        Callback hook, called when whether the component is configured
+        changes
+
+        :param configured: whether this component is configured
+        :type configured: bool
+        """
+        if configured:
+            self.obs_state_model.perform_action("component_configured")
+        else:
+            self.obs_state_model.perform_action("component_unconfigured")
+
+    def component_scanning(self, scanning):
+        """
+        Callback hook, called when whether the component is scanning
+        changes
+
+        :param scanning: whether this component is scanning
+        :type scanning: bool
+        """
+        if scanning:
+            self.obs_state_model.perform_action("component_scanning")
+        else:
+            self.obs_state_model.perform_action("component_not_scanning")
+
+    def component_obsfault(self):
+        """
+        Callback hook, called when the component obsfaults
+        """
+        self.obs_state_model.perform_action("component_obsfault")
diff --git a/src/ska_tango_base/csp_subelement_obsdevice.py b/src/ska_tango_base/csp/obs/obs_device.py
similarity index 69%
rename from src/ska_tango_base/csp_subelement_obsdevice.py
rename to src/ska_tango_base/csp/obs/obs_device.py
index ebdec29549b5624bd1cb8ed5e24638af347e86b2..060ecebe1c233b8cc1c6c69ecda4d8309c30ed5f 100644
--- a/src/ska_tango_base/csp_subelement_obsdevice.py
+++ b/src/ska_tango_base/csp/obs/obs_device.py
@@ -10,81 +10,25 @@ General observing device for SKA CSP Subelement.
 
 # PROTECTED REGION ID(CspSubElementObsDevice.additionnal_import) ENABLED START #
 import json
-import warnings
-import numpy as np
 from json.decoder import JSONDecodeError
 
 # Tango imports
-import tango
-from tango import DebugIt, DevState, AttrWriteType
+from tango import DebugIt
 from tango.server import run, attribute, command, device_property
 
 # SKA specific imports
-from ska_tango_base import SKAObsDevice, ObsDeviceStateModel
-from ska_tango_base.commands import ResultCode, ActionCommand
+from ska_tango_base import SKAObsDevice
+from ska_tango_base.commands import (
+    ResultCode,
+    CompletionCommand,
+    ObservationCommand,
+    ResponseCommand,
+)
 from ska_tango_base.control_model import ObsState
-from ska_tango_base.faults import CommandError
-from ska_tango_base.csp_subelement_state_machine import CspSubElementObsDeviceStateMachine
+from ska_tango_base.csp.obs import CspObsComponentManager, CspSubElementObsStateModel
 
-__all__ = ["CspSubElementObsDevice", "CspSubElementObsDeviceStateModel", "main"]
 
-
-class CspSubElementObsDeviceStateModel(ObsDeviceStateModel):
-    """
-    Implements the state model for the CspSubElementObsDevice.
-
-    :param logger: the logger to be used by this state model.
-    :type logger: a logger that implements the standard library
-       logger interface
-    :param op_state_callback: A callback to be called when a
-       transition implies a change to op state
-    :type op_state_callback: callable
-    :param admin_mode_callback: A callback to be called when a
-       transition causes a change to device admin_mode
-    :type admin_mode_callback: callable
-    :param obs_state_callback: A callback to be called when a
-       transition causes a change to device obs_state
-    :type obs_state_callback: callable
-    """
-
-    def __init__(
-        self,
-        logger,
-        op_state_callback=None,
-        admin_mode_callback=None,
-        obs_state_callback=None,
-    ):
-        action_breakdown = {
-            # "action": ("action_on_obs_machine", "action_on_superclass"),
-            "off_succeeded": ("to_IDLE", "off_succeeded"),
-            "off_failed": ("to_IDLE", "off_failed"),
-            "on_succeeded": (None, "on_succeeded"),
-            "on_failed": ("to_IDLE", "on_failed"),
-            "configure_started": ("configure_started", None),
-            "configure_succeeded": ("configure_succeeded", None),
-            "configure_failed": ("configure_failed", None),
-            "scan_started": ("scan_started", None),
-            "scan_succeeded": ("scan_succeeded", None),
-            "scan_failed": ("scan_failed", None),
-            "end_scan_succeeded": ("end_scan_succeeded", None),
-            "end_scan_failed": ("end_scan_failed", None),
-            "end_succeeded": ("end_succeeded", None),
-            "end_failed": ("end_failed", None),
-            "abort_started": ("abort_started", None),
-            "abort_succeeded": ("abort_succeeded", None),
-            "abort_failed": ("abort_failed", None),
-            "obs_reset_succeeded": ("reset_succeeded", None),
-            "obs_reset_failed": ("reset_failed", None),
-            "fatal_error": ("fatal_error", None),
-        }
-        super().__init__(
-            action_breakdown,
-            CspSubElementObsDeviceStateMachine,
-            logger,
-            op_state_callback=op_state_callback,
-            admin_mode_callback=admin_mode_callback,
-            obs_state_callback=obs_state_callback,
-        )
+__all__ = ["CspSubElementObsDevice", "main"]
 
 
 class CspSubElementObsDevice(SKAObsDevice):
@@ -181,37 +125,38 @@ class CspSubElementObsDevice(SKAObsDevice):
         """
         Sets up the state model for the device
         """
-        self.state_model = CspSubElementObsDeviceStateModel(
+        super()._init_state_model()
+        self.obs_state_model = CspSubElementObsStateModel(
             logger=self.logger,
-            op_state_callback=self._update_state,
-            admin_mode_callback=self._update_admin_mode,
-            obs_state_callback=self._update_obs_state,
+            callback=self._update_obs_state,
         )
 
+    def create_component_manager(self):
+        return CspObsComponentManager(self.op_state_model, self.obs_state_model)
+
     def init_command_objects(self):
         """
         Sets up the command objects
         """
         super().init_command_objects()
-        device_args = (self, self.state_model, self.logger)
-        self.register_command_object(
-            "ConfigureScan", self.ConfigureScanCommand(*device_args)
-        )
-        self.register_command_object(
-            "Scan", self.ScanCommand(*device_args)
-        )
-        self.register_command_object(
-            "EndScan", self.EndScanCommand(*device_args)
-        )
-        self.register_command_object(
-            "GoToIdle", self.GoToIdleCommand(*device_args)
-        )
-        self.register_command_object(
-            "Abort", self.AbortCommand(*device_args)
-        )
-        self.register_command_object(
-            "ObsReset", self.ObsResetCommand(*device_args)
-        )
+
+        for (command_name, command_class) in [
+            ("ConfigureScan", self.ConfigureScanCommand),
+            ("Scan", self.ScanCommand),
+            ("EndScan", self.EndScanCommand),
+            ("GoToIdle", self.GoToIdleCommand),
+            ("Abort", self.AbortCommand),
+            ("ObsReset", self.ObsResetCommand),
+        ]:
+            self.register_command_object(
+                command_name,
+                command_class(
+                    self.component_manager,
+                    self.op_state_model,
+                    self.obs_state_model,
+                    self.logger,
+                )
+            )
 
     class InitCommand(SKAObsDevice.InitCommand):
         """
@@ -231,7 +176,6 @@ class CspSubElementObsDevice(SKAObsDevice):
 
             device = self.target
             device._obs_state = ObsState.IDLE
-            device._scan_id = 0
 
             device._sdp_addresses = {"outputHost": [], "outputMac": [], "outputPort": []}
             # a sub-element obsdevice can have more than one link to the SDP
@@ -239,7 +183,7 @@ class CspSubElementObsDevice(SKAObsDevice):
             device._sdp_links_active = [False, ]
             device._sdp_links_capacity = 0.
 
-            device._config_id = ''
+            # JSON string, deliberately left in Tango layer
             device._last_scan_configuration = ''
             device._health_failure_msg = ''
 
@@ -248,7 +192,7 @@ class CspSubElementObsDevice(SKAObsDevice):
             return (ResultCode.OK, message)
 
     def always_executed_hook(self):
-        """Method always executed before any TANGO command is executed."""
+        """Method always executed before any Tango command is executed."""
         # PROTECTED REGION ID(CspSubElementObsDevice.always_executed_hook) ENABLED START #
         # PROTECTED REGION END #    //  CspSubElementObsDevice.always_executed_hook
 
@@ -268,13 +212,13 @@ class CspSubElementObsDevice(SKAObsDevice):
     def read_scanID(self):
         # PROTECTED REGION ID(CspSubElementObsDevice.scanID_read) ENABLED START #
         """Return the scanID attribute."""
-        return self._scan_id
+        return self.component_manager.scan_id  #pylint: disable=no-member
         # PROTECTED REGION END #    //  CspSubElementObsDevice.scanID_read
 
     def read_configurationID(self):
         # PROTECTED REGION ID(CspSubElementObsDevice.configurationID_read) ENABLED START #
         """Return the configurationID attribute."""
-        return self._config_id
+        return self.component_manager.config_id  #pylint: disable=no-member
         # PROTECTED REGION END #    //  CspSubElementObsDevice.configurationID_read
 
     def read_deviceID(self):
@@ -317,53 +261,49 @@ class CspSubElementObsDevice(SKAObsDevice):
     # Commands
     # --------
 
-    class ConfigureScanCommand(ActionCommand):
+    class ConfigureScanCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for the CspSubElementObsDevices's ConfigureScan command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ConfigureScanCommand
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`CspSubElementObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "configure", start_action=True, logger=logger
+                target, obs_state_model, "configure", op_state_model, logger=logger
             )
 
         def do(self, argin):
             """
             Stateless hook for ConfigureScan() command functionality.
 
-            :param argin: The configuration as JSON formatted string
-            :type argin: str
+            :param argin: The configuration
+            :type argin: dict
 
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
             :rtype: (ResultCode, str)
-            :raises: ``CommandError`` if the configuration data validation fails.
             """
-            device = self.target
-            # validate the input args
-            (result_code, msg) = self.validate_input(argin)
-            if result_code == ResultCode.OK:
-                # store the configuration on command success
-                device._last_scan_configuration = argin
-                msg = "Configure command completed OK"
-            return(result_code, msg)
+            component_manager = self.target
+            component_manager.configure_scan(argin)
+            return (ResultCode.OK, "Configure command completed OK")
 
         def validate_input(self, argin):
             """
@@ -374,45 +314,45 @@ class CspSubElementObsDevice(SKAObsDevice):
             :return: A tuple containing a return code and a string message.
             :rtype: (ResultCode, str)
             """
-            device = self.target
             try:
                 configuration_dict = json.loads(argin)
-                device._config_id = configuration_dict['id']
-                # call the method to validate the data sent with
-                # the configuration, as needed.
-                return (ResultCode.OK, "ConfigureScan arguments validation successfull")
+                _ = configuration_dict["id"]
             except (KeyError, JSONDecodeError) as err:
-                msg = "Validate configuration failed with error:{}".format(err)
-            except Exception as other_errs:
-                msg = "Validate configuration failed with unknown error:{}".format(
-                    other_errs)
+                msg = f"Validate configuration failed with error:{err}"
                 self.logger.error(msg)
-            return (ResultCode.FAILED, msg)
+                return (None, ResultCode.FAILED, msg)
+            except Exception as other_errs:
+                msg = f"Validate configuration failed with unknown error: {other_errs}"
+                return (None, ResultCode.FAILED, msg)
 
-    class ScanCommand(ActionCommand):
+            return (configuration_dict, ResultCode.OK, "ConfigureScan arguments validation successful")
+
+    class ScanCommand(ObservationCommand, ResponseCommand):
         """
         A class for the CspSubElementObsDevices's Scan command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ScanCommand
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "scan", start_action=True, logger=logger
+                target, obs_state_model, "scan", op_state_model, logger=logger
             )
 
         def do(self, argin):
@@ -427,11 +367,10 @@ class CspSubElementObsDevice(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
+            component_manager = self.target
             (result_code, msg) = self.validate_input(argin)
             if result_code == ResultCode.OK:
-                # store the configuration on command success
-                device._scan_id = int(argin)
+                component_manager.scan(int(argin))
                 return (ResultCode.STARTED, "Scan command started")
             return(result_code, msg)
 
@@ -452,30 +391,32 @@ class CspSubElementObsDevice(SKAObsDevice):
                 return (ResultCode.FAILED, msg)
             return (ResultCode.OK, "Scan arguments validation successfull")
 
-    class EndScanCommand(ActionCommand):
+    class EndScanCommand(ObservationCommand, ResponseCommand):
         """
         A class for the CspSubElementObsDevices's EndScan command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for EndScanCommand
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "end_scan", logger=logger
+                target, obs_state_model, "end_scan", op_state_model, logger=logger
             )
 
         def do(self):
@@ -487,32 +428,36 @@ class CspSubElementObsDevice(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            component_manager = self.target
+            component_manager.end_scan()
             return (ResultCode.OK, "EndScan command completed OK")
 
-    class GoToIdleCommand(ActionCommand):
+    class GoToIdleCommand(ObservationCommand, ResponseCommand):
         """
         A class for the CspSubElementObsDevices's GoToIdle command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
-            Constructor for GoToIdle Command.
+            Constructor for EndCommand
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "end", logger=logger
+                target, obs_state_model, "end", op_state_model, logger=logger
             )
 
         def do(self):
@@ -524,39 +469,36 @@ class CspSubElementObsDevice(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-            if device.state_model.obs_state == ObsState.IDLE:
-                return (ResultCode.OK, "GoToIdle command completed OK. Device already IDLE")
-            # reset to default values the configurationID and scanID
-            device._config_id = ''
-            device._scan_id = 0
-            device._last_scan_configuration = ''
+            component_manager = self.target
+            component_manager.deconfigure()
             return (ResultCode.OK, "GoToIdle command completed OK")
 
-    class ObsResetCommand(ActionCommand):
+    class ObsResetCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for the CspSubElementObsDevices's ObsReset command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ObsReset Command.
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`CspSubElementObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "obs_reset", logger=logger
+                target, obs_state_model, "obsreset", op_state_model, logger=logger
             )
 
         def do(self):
@@ -572,30 +514,32 @@ class CspSubElementObsDevice(SKAObsDevice):
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class AbortCommand(ActionCommand):
+    class AbortCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for the CspSubElementObsDevices's Abort command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for Abort Command.
 
             :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`CspSubElementObsStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`CspSubElementObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "abort", start_action=True, logger=logger
+                target, obs_state_model, "abort", op_state_model, logger=logger
             )
 
         def do(self):
@@ -630,8 +574,14 @@ class CspSubElementObsDevice(SKAObsDevice):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("ConfigureScan")
-        (return_code, message) = command(argin)
-        return [[return_code], [message]]
+
+        (configuration, result_code, message) = command.validate_input(argin)
+        if result_code == ResultCode.OK:
+            # store the configuration on command success
+            self._last_scan_configuration = argin
+            (result_code, message) = command(configuration)
+
+        return [[result_code], [message]]
         # PROTECTED REGION END #    //  CspSubElementObsDevice.ConfigureScan
 
     @command(
@@ -694,6 +644,8 @@ class CspSubElementObsDevice(SKAObsDevice):
             The message is for information purpose only.
         :rtype: (ResultCode, str)
         """
+        self._last_scan_configuration = ''
+
         command = self.get_command_object("GoToIdle")
         (return_code, message) = command()
         return [[return_code], [message]]
diff --git a/src/ska_tango_base/csp/obs/obs_state_model.py b/src/ska_tango_base/csp/obs/obs_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..93fdcd19528a892446aa23fdccdb377135e25fcb
--- /dev/null
+++ b/src/ska_tango_base/csp/obs/obs_state_model.py
@@ -0,0 +1,345 @@
+"""
+This module specifies CSP SubElement Observing state machine. It
+comprises:
+
+* an underlying state machine:
+  :py:class:`._CspSubElementObsStateMachine`
+
+* a :py:class:`.CspSubElementObsStateModel` that maps the underlying
+  state machine state to a value of the
+  :py:class:`ska_tango_base.control_model.ObsState` enum.
+  
+"""
+from transitions.extensions import LockedMachine as Machine
+
+from ska_tango_base.control_model import ObsState
+from ska_tango_base.obs import ObsStateModel
+
+__all__ = ["CspSubElementObsStateModel"]
+
+
+class _CspSubElementObsStateMachine(Machine):
+    """
+    The observation state machine used by a generic CSP sub-element
+    ObsDevice (derived from SKAObsDevice).
+
+    Compared to the SKA Observation State Machine, it implements a
+    smaller number of states, number that can be further decreased
+    depending on the necessities of the different sub-elements.
+
+    The implemented states are:
+
+    * **IDLE**: the device is unconfigured.
+
+    * **CONFIGURING_IDLE**: the device in unconfigured, but
+      configuration is in progress.
+
+    * **CONFIGURING_READY**: the device in configured, and configuration
+      is in progress.
+
+    * **READY**: the device is configured and is ready to perform
+      observations
+
+    * **SCANNING**: the device is performing the observation.
+
+    * **ABORTING**: the device is processing an abort.
+
+      TODO: Need to understand if this state is really required by the
+      observing devices of any CSP sub-element.
+
+    * **ABORTED**: the device has completed the abort request.
+
+    * **FAULT**: the device component has experienced an error from
+      which it can be recovered only via manual intervention invoking a
+      reset command that force the device to the base state (IDLE).
+
+    The actions supported divide into command-oriented actions and
+    component monitoring actions.
+
+    The command-oriented actions are:
+
+    * **configure_invoked** and **configure_completed**: bookending the
+      Configure() command, and hence the CONFIGURING state
+    * **abort_invoked** and **abort_completed**: bookending the Abort()
+      command, and hence the ABORTING state
+    * **obsreset_invoked** and **obsreset_completed**: bookending the
+      ObsReset() command, and hence the OBSRESETTING state
+    * **end_invoked**, **scan_invoked**, **end_scan_invoked**: these
+      result in reflexive transitions, and are purely there to indicate
+      states in which the End(), Scan() and EndScan() commands are
+      permitted to be run
+
+    The component-oriented actions are:
+
+    * **component_obsfault**: the monitored component has experienced an
+      observation fault
+    * **component_unconfigured**: the monitored component has become
+      unconfigured
+    * **component_configured**: the monitored component has become
+      configured
+    * **component_scanning**: the monitored component has started
+      scanning
+    * **component_not_scanning**: the monitored component has stopped
+      scanning
+
+    A diagram of the state machine is shown below. Reflexive transitions
+    and transitions to FAULT obs state are omitted to simplify the
+    diagram.
+
+    .. uml:: csp_subelement_obs_state_machine.uml
+       :caption: Diagram of the CSP subelement obs state machine
+
+    The following is a diagram of the state machine, automatically
+    generated from the code. Its equivalence to the diagram above
+    demonstrates that the implementation is faithful to the design.
+
+    .. figure:: _CspSubElementObsStateMachine_autogenerated.png
+      :alt: Diagram of the CSP subelement obs state machine, as implemented
+
+    """
+
+    def __init__(self, callback=None, **extra_kwargs):
+        """
+        Initialises the model.
+
+        :param callback: A callback to be called when the state changes
+        :type callback: callable
+        :param extra_kwargs: Additional keywords arguments to pass to super class
+            initialiser (useful for graphing)
+        """
+        self._callback = callback
+
+        states = [
+            "IDLE",
+            "CONFIGURING_IDLE",  # device CONFIGURING but component is unconfigured
+            "CONFIGURING_READY",  # device CONFIGURING and component is configured
+            "READY",
+            "SCANNING",
+            "ABORTING",
+            "ABORTED",
+            "RESETTING",
+            "FAULT",
+        ]
+        transitions = [
+            {
+                "source": "*",
+                "trigger": "component_obsfault",
+                "dest": "FAULT",
+            },
+            {
+                "source": "IDLE",
+                "trigger": "configure_invoked",
+                "dest": "CONFIGURING_IDLE",
+            },
+            {
+                "source": "CONFIGURING_IDLE",
+                "trigger": "configure_completed",
+                "dest": "IDLE",
+            },
+            {
+                "source": "READY",
+                "trigger": "configure_invoked",
+                "dest": "CONFIGURING_READY",
+            },
+            {
+                "source": "CONFIGURING_IDLE",
+                "trigger": "component_configured",
+                "dest": "CONFIGURING_READY",
+            },
+            {
+                "source": "CONFIGURING_READY",
+                "trigger": "configure_completed",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "end_invoked",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "component_unconfigured",
+                "dest": "IDLE",
+            },
+            {
+                "source": "READY",
+                "trigger": "scan_invoked",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "component_scanning",
+                "dest": "SCANNING",
+            },
+            {
+                "source": "SCANNING",
+                "trigger": "end_scan_invoked",
+                "dest": "SCANNING",
+            },
+            {
+                "source": "SCANNING",
+                "trigger": "component_not_scanning",
+                "dest": "READY",
+            },
+            {
+                "source": [
+                    "IDLE",
+                    "CONFIGURING_IDLE",
+                    "CONFIGURING_READY",
+                    "READY",
+                    "SCANNING",
+                    "RESETTING",
+                ],
+                "trigger": "abort_invoked",
+                "dest": "ABORTING",
+            },
+            # Aborting implies trying to stop the monitored component
+            # while it is doing something. Thus the monitored component
+            # may send some events while in aborting state.
+            {
+                "source": "ABORTING",
+                "trigger": "component_unconfigured",  # Abort() invoked on ObsReset()
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "component_configured",  # Configure() was just finishing
+                "dest": "ABORTING",
+            },
+            {
+                "source": ["ABORTING"],
+                "trigger": "component_not_scanning",  # Aborting implies stopping scan
+                "dest": "ABORTING",
+            },
+            {
+                "source": ["ABORTING"],
+                "trigger": "component_scanning",  # Abort() invoked as scan is starting
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "abort_completed",
+                "dest": "ABORTED",
+            },
+            {
+                "source": ["ABORTED", "FAULT"],
+                "trigger": "obsreset_invoked",
+                "dest": "RESETTING",
+            },
+            {
+                "source": "RESETTING",
+                "trigger": "component_unconfigured",  # Resetting implies deconfiguring
+                "dest": "RESETTING",
+            },
+            {
+                "source": "RESETTING",
+                "trigger": "obsreset_completed",
+                "dest": "IDLE",
+            },
+        ]
+        super().__init__(
+            states=states,
+            initial="IDLE",
+            transitions=transitions,
+            after_state_change=self._state_changed,
+            **extra_kwargs,
+        )
+        self._state_changed()
+
+    def _state_changed(self):
+        """
+        State machine callback that is called every time the obs_state
+        changes. Responsible for ensuring that callbacks are called.
+        """
+        if self._callback is not None:
+            self._callback(self.state)
+
+
+class CspSubElementObsStateModel(ObsStateModel):
+    """
+    Implements the observation state model for a generic CSP sub-element
+    ObsDevice (derived from SKAObsDevice).
+
+    Compared to the SKA observation state model, it implements a
+    smaller number of states, number that can be further decreased
+    depending on the necessities of the different sub-elements.
+
+    The implemented states are:
+
+    * **IDLE**: the device is unconfigured.
+
+    * **CONFIGURING**: transitional state to report device configuration
+      is in progress.
+      
+      TODO: Need to understand if this state is really required by the
+      observing devices of any CSP sub-element.
+
+    * **READY**: the device is configured and is ready to perform
+      observations
+
+    * **SCANNING**: the device is performing the observation.
+
+    * **ABORTING**: the device is processing an abort.
+
+      TODO: Need to understand if this state is really required by the
+      observing devices of any CSP sub-element.
+
+    * **ABORTED**: the device has completed the abort request.
+
+    * **RESETTING**: the device is resetting from an ABORTED or FAULT
+      state back to IDLE
+
+    * **FAULT**: the device component has experienced an error from
+      which it can be recovered only via manual intervention invoking a
+      reset command that force the device to the base state (IDLE).
+
+    A diagram of the CSP subelement observation state model is shown
+    below. This model is non-deterministic as diagrammed, but the
+    underlying state machine has extra state and transitions that render
+    it deterministic. This model class simply maps those extra classes
+    onto valid ObsState values
+
+    .. uml:: obs_state_model.uml
+       :caption: Diagram of the observation state model
+    """
+
+    def __init__(self, logger, callback=None):
+        """
+        Initialise the model.
+
+        :param logger: the logger to be used by this state model.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param callback: A callback to be called when a transition
+            causes a change to device obs_state
+        :type callback: callable
+        """
+        super().__init__(_CspSubElementObsStateMachine, logger, callback=callback)
+
+    _obs_state_mapping = {
+        "IDLE": ObsState.IDLE,
+        "CONFIGURING_IDLE": ObsState.CONFIGURING,
+        "CONFIGURING_READY": ObsState.CONFIGURING,
+        "READY": ObsState.READY,
+        "SCANNING": ObsState.SCANNING,
+        "ABORTING": ObsState.ABORTING,
+        "ABORTED": ObsState.ABORTED,
+        "RESETTING": ObsState.RESETTING,
+        "FAULT": ObsState.FAULT,
+    }
+
+    def _obs_state_changed(self, machine_state):
+        """
+        Helper method that updates obs_state whenever the observation
+        state machine reports a change of state, ensuring that the
+        callback is called if one exists.
+
+        :param machine_state: the new state of the observation state
+            machine
+        :type machine_state: str
+        """
+        obs_state = self._obs_state_mapping[machine_state]
+        if self._obs_state != obs_state:
+            self._obs_state = obs_state
+            if self._callback is not None:
+                self._callback(obs_state)
diff --git a/src/ska_tango_base/csp/obs/reference_component_manager.py b/src/ska_tango_base/csp/obs/reference_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..4210f632537a745f78f5ff6872b36abfa5997e40
--- /dev/null
+++ b/src/ska_tango_base/csp/obs/reference_component_manager.py
@@ -0,0 +1,325 @@
+"""
+This module models component management for CSP subelement observation devices.
+"""
+import functools
+
+from tango import DevState
+
+from ska_tango_base.csp.obs import CspObsComponentManager
+from ska_tango_base.base import check_communicating, ReferenceBaseComponentManager
+
+from ska_tango_base.control_model import PowerMode
+from ska_tango_base.faults import ComponentError, ComponentFault
+
+
+def check_on(func):
+    """
+    Decorator that makes a method first checks that the component is
+    turned on and not faulty before allowing the command to proceed
+
+    :param func: the wrapped function
+
+    :return: the wrapped function
+    """
+    @functools.wraps(func)
+    def _wrapper(component, *args, **kwargs):
+        """
+        Wrapper function that checks that the component is turned on and
+        not faulty before invoking the wrapped function
+
+        :param component: the component to check
+        :param args: positional arguments to the wrapped function
+        :param kwargs: keyword arguments to the wrapped function
+
+        :return: whatever the wrapped function returns
+        """
+        if component.faulty:
+            raise ComponentFault()
+        if component.power_mode != PowerMode.ON:
+            raise ComponentError("Component is not ON")
+        return func(component, *args, **kwargs)
+
+    return _wrapper
+
+
+class ReferenceCspObsComponentManager(CspObsComponentManager, ReferenceBaseComponentManager):
+    """
+    A component manager for SKA CSP subelement observation Tango devices:
+
+    The current implementation is intended to
+    * illustrate the model
+    * enable testing of the base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a subclass specific to the component managed by the device.
+    """
+
+    class _Component(ReferenceBaseComponentManager._Component):
+        """
+        An example CSP subelement obs component for the component
+        manager to work with.
+
+        It can be directly controlled via configure(), scan(),
+        end_scan(), go_to_idle(), abort() and reset()  command methods.
+
+        For testing purposes, it can also be told to simulate an
+        observation fault via simulate_obsfault() methods.
+
+        When a component changes state, it lets the component manager
+        know by calling its ``component_unconfigured``,
+        ``component_configured``, ``component_scanning``,
+        ``component_not_scanning`` and ``component_obsfault`` methods.
+        """
+
+        def __init__(
+            self,
+            _power_mode=PowerMode.OFF,
+            _faulty=False,
+        ):
+            """
+            Initialise a new instance
+   State Machine<State_Machine>
+
+
+            :param _power_mode: initial power mode of this component
+                (for testing only)
+            :param _faulty: whether this component should initially
+                simulate a fault (for testing only)
+            """
+            self._configured = False
+            self._configured_callback = None
+            self._config_id = ""
+
+            self._scanning = False
+            self._scanning_callback = None
+            self._scan_id = 0
+
+            self._obsfault = False
+            self._obsfault_callback = None
+
+            super().__init__(_power_mode=_power_mode, _faulty=_faulty)
+
+        def set_obs_callbacks(
+            self, configured_callback, scanning_callback, obsfault_callback
+        ):
+            self._configured_callback = configured_callback
+            self._scanning_callback = scanning_callback
+            self._obsfault_callback = obsfault_callback
+
+        @property
+        @check_on
+        def configured(self):
+            return self._configured
+
+        @property
+        @check_on
+        def config_id(self):
+            return self._config_id
+
+        # @config_id.setter
+        # @check_on
+        # def config_id(self, config_id):
+        #     self._config_id = config_id
+
+        @property
+        @check_on
+        def scanning(self):
+            return self._scanning
+
+        @property
+        @check_on
+        def scan_id(self):
+            return self._scan_id
+
+        @property
+        @check_on
+        def obsfault(self):
+            return self._obsfault
+
+        @check_on
+        def configure_scan(self, configuration):
+            self._config_id = configuration["id"]
+            self._update_configured(True)
+
+        @check_on
+        def deconfigure(self):
+            self._config_id = ""
+            self._update_configured(False)
+
+        @check_on
+        def scan(self, scan_id):
+            self._scan_id = scan_id
+            self._update_scanning(True)
+
+        @check_on
+        def end_scan(self):
+            self.simulate_scan_stopped()
+
+        @check_on
+        def simulate_scan_stopped(self):
+            self._scan_id = 0
+            self._update_scanning(False)
+
+        @check_on
+        def simulate_obsfault(self, obsfault):
+            self._update_obsfault(obsfault)
+
+        def _invoke_configured_callback(self):
+            if not self.faulty:
+                if self._configured_callback is not None:
+                    self._configured_callback(self._configured)
+
+        def _update_configured(self, configured):
+            if self._configured != configured:
+                self._configured = configured
+                self._invoke_configured_callback()
+
+        def _invoke_scanning_callback(self):
+            if not self.faulty:
+                if self._scanning_callback is not None:
+                    self._scanning_callback(self._scanning)
+
+        def _update_scanning(self, scanning):
+            if self._scanning != scanning:
+                self._scanning = scanning
+                self._invoke_scanning_callback()
+
+        def _invoke_obsfault_callback(self):
+            if not self.faulty:
+                if self.obsfault and self._obsfault_callback is not None:
+                    self._obsfault_callback()
+
+        def _update_obsfault(self, obsfault):
+            if self._obsfault != obsfault:
+                self._obsfault = obsfault
+                if obsfault:
+                    self._invoke_obsfault_callback()
+
+    def __init__(self, op_state_model, obs_state_model, logger=None, _component=None):
+        super().__init__(
+            op_state_model,
+            obs_state_model,
+            logger=logger,
+            _component=_component or self._Component(),
+        )
+
+    def start_communicating(self):
+        """
+        Establish communication with the component, then start
+        monitoring.
+        """
+        if self._connected:
+            return
+
+        super().start_communicating()
+
+        self._component.set_obs_callbacks(
+            self.component_configured,
+            self.component_scanning,
+            self.component_obsfault,
+        )
+
+        if self._component.faulty:
+            return
+        if self._component.power_mode != PowerMode.ON:
+            return
+
+        # we've been disconnected and we might have missed some
+        # changes, so we need to check the component's state, and
+        # make our state model correspond
+        if self._component.obsfault:
+            self.op_state_model.to_OBSFAULT()
+        elif not self._component.configured:
+            self.op_state_model.to_IDLE()
+        elif not self._component.scanning:
+            self.op_state_model.to_READY()
+        else:
+            self.op_state_model.to_SCANNING()
+
+    def stop_communicating(self):
+        """
+        Cease monitoring the component, and break off all communication
+        with it.
+        """
+        if not self._connected:
+            return
+
+        self._component.set_obs_callbacks(None, None, None, None)
+        super().stop_communicating()
+
+    def simulate_communication_failure(self, fail_communicate):
+        """
+        Simulate (or stop simulating) a failure to communicate with the
+        component
+
+        :param fail_communicate: whether the connection to the component
+            is failing
+        """
+        # Pretend that we have either tried to connect a disconnected
+        # device and failed; or realised that our connection to the
+        # device has been broken
+        if fail_communicate and self._connected:
+            self._component.set_obs_callbacks(None, None, None, None)
+        super().simulate_communication_failure(fail_communicate)
+
+    @check_communicating
+    def configure_scan(self, configuration):
+        self.logger.info("Configuring component")
+        self._component.configure_scan(configuration)
+
+    @check_communicating
+    def deconfigure(self):
+        self.logger.info("Deconfiguring component")
+        self._component.deconfigure()
+
+    @check_communicating
+    def scan(self, args):
+        self.logger.info("Starting scan in component")
+        self._component.scan(args)
+
+    @check_communicating
+    def end_scan(self):
+        self.logger.info("Stopping scan in component")
+        self._component.end_scan()
+
+    @check_communicating
+    def abort(self):
+        self.logger.info("Aborting component")
+        if self._component.scanning:
+            self._component.end_scan()
+
+    @check_communicating
+    def obsreset(self):
+        self.logger.info("Resetting component")
+        if self._component.configured:
+            self._component.deconfigure()
+
+    @property
+    @check_communicating
+    def config_id(self):
+        return self._component.config_id
+
+    @property
+    @check_on
+    def scan_id(self):
+        return self._component.scan_id
+
+    @config_id.setter
+    @check_communicating
+    def config_id(self, config_id):
+        self._component.config_id = config_id
+
+    def component_configured(self, configured):
+        if configured:
+            self.obs_state_model.perform_action("component_configured")
+        else:
+            self.obs_state_model.perform_action("component_unconfigured")
+
+    def component_scanning(self, scanning):
+        if scanning:
+            self.obs_state_model.perform_action("component_scanning")
+        else:
+            self.obs_state_model.perform_action("component_not_scanning")
+
+    def component_obsfault(self):
+        self.obs_state_model.perform_action("component_obsfault")
diff --git a/src/ska_tango_base/csp/subarray/__init__.py b/src/ska_tango_base/csp/subarray/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d936792f3d386c16d45fec777a8b4a2f9e9167c
--- /dev/null
+++ b/src/ska_tango_base/csp/subarray/__init__.py
@@ -0,0 +1,16 @@
+"""
+This subpackage contains subarray device functionality specific to CSP.
+"""
+
+__all__ = (
+    "CspSubarrayComponentManager",
+    "ReferenceCspSubarrayComponentManager",
+    "CspSubElementSubarray",
+)
+
+# Note: order of imports is important - start with lowest in the hierarchy
+from .component_manager import CspSubarrayComponentManager
+
+from .reference_component_manager import ReferenceCspSubarrayComponentManager
+
+from .subarray_device import CspSubElementSubarray
diff --git a/src/ska_tango_base/csp/subarray/component_manager.py b/src/ska_tango_base/csp/subarray/component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..cab4cf55bf07b7c2a43925393e77ff23b2fcfd64
--- /dev/null
+++ b/src/ska_tango_base/csp/subarray/component_manager.py
@@ -0,0 +1,33 @@
+"""
+This module models component management for CSP subarrays.
+"""
+from ska_tango_base.subarray import SubarrayComponentManager
+
+
+class CspSubarrayComponentManager(SubarrayComponentManager):
+    """
+    A component manager for SKA CSP subarray Tango devices:
+
+    The current implementation is intended to
+    * illustrate the model
+    * enable testing of the base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a subclass specific to the component managed by the device.
+    """
+
+    def __init__(self, op_state_model, obs_state_model, *args, **kwargs):
+        super().__init__(
+            op_state_model,
+            obs_state_model,
+            *args,
+            **kwargs,
+        )
+
+    @property
+    def config_id(self):
+        return NotImplementedError("CspSubarrayComponentManager is abstract.")
+
+    @property
+    def scan_id(self):
+        return NotImplementedError("CspSubarrayComponentManager is abstract.")
diff --git a/src/ska_tango_base/csp/subarray/reference_component_manager.py b/src/ska_tango_base/csp/subarray/reference_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..e2d9b4815eae005889793b12ebed331c76e9a77c
--- /dev/null
+++ b/src/ska_tango_base/csp/subarray/reference_component_manager.py
@@ -0,0 +1,146 @@
+"""
+This module models component management for CSP subelement observation devices.
+"""
+import functools
+
+from ska_tango_base.base import check_communicating
+from ska_tango_base.csp.subarray import CspSubarrayComponentManager
+from ska_tango_base.subarray import ReferenceSubarrayComponentManager
+from ska_tango_base.control_model import PowerMode
+from ska_tango_base.faults import ComponentError, ComponentFault
+
+
+def check_on(func):
+    """
+    Decorator that makes a method first checks that the component is
+    turned on and not faulty before allowing the command to proceed
+
+    :param func: the wrapped function
+
+    :return: the wrapped function
+    """
+    @functools.wraps(func)
+    def _wrapper(component, *args, **kwargs):
+        """
+        Wrapper function that checks that the component is turned on and
+        not faulty before invoking the wrapped function
+
+        :param component: the component to check
+        :param args: positional arguments to the wrapped function
+        :param kwargs: keyword arguments to the wrapped function
+
+        :return: whatever the wrapped function returns
+        """
+        if component.faulty:
+            raise ComponentFault()
+        if component.power_mode != PowerMode.ON:
+            raise ComponentError("Component is not ON")
+        return func(component, *args, **kwargs)
+
+    return _wrapper
+
+
+class ReferenceCspSubarrayComponentManager(
+    CspSubarrayComponentManager,
+    ReferenceSubarrayComponentManager,
+):
+    """
+    A component manager for SKA CSP subelement observation Tango devices:
+
+    The current implementation is intended to
+    * illustrate the model
+    * enable testing of the base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a subclass specific to the component managed by the device.
+    """
+
+    class _Component(ReferenceSubarrayComponentManager._Component):
+        """
+        An example CSP subelement obs component for the component
+        manager to work with.
+
+        It can be directly controlled via configure(), scan(),
+        end_scan(), go_to_idle(), abort() and reset()  command methods.
+
+        For testing purposes, it can also be told to simulate an
+        observation fault via simulate_obsfault() methods.
+
+        When a component changes state, it lets the component manager
+        know by calling its ``component_unconfigured``,
+        ``component_configured``, ``component_scanning``,
+        ``component_not_scanning`` and ``component_obsfault`` methods.
+        """
+
+        def __init__(
+            self,
+            capability_types,
+            _power_mode=PowerMode.OFF,
+            _faulty=False,
+        ):
+            """
+            Initialise a new instance
+
+            :param capability_types: a list strings representing
+                capability types.
+            :param _power_mode: initial power mode of this component
+                (for testing only)
+            :param _faulty: whether this component should initially
+                simulate a fault (for testing only)
+            """
+            self._config_id = ""
+            self._scan_id = 0
+
+            super().__init__(capability_types, _power_mode=_power_mode, _faulty=_faulty)
+
+        @property
+        @check_on
+        def config_id(self):
+            return self._config_id
+
+        @property
+        @check_on
+        def scan_id(self):
+            return self._scan_id
+
+        @check_on
+        def configure(self, configuration):
+            self._config_id = configuration["id"]
+            self._update_configured(True)
+
+        @check_on
+        def deconfigure(self):
+            self._config_id = ""
+            super().deconfigure()
+
+        @check_on
+        def scan(self, scan_id):
+            self._scan_id = scan_id
+            super().scan(scan_id)
+
+        @check_on
+        def simulate_scan_stopped(self):
+            self._scan_id = 0
+            super().simulate_scan_stopped()
+
+    def __init__(
+        self, op_state_model, obs_state_model, capability_types, logger, _component=None
+    ):
+        super().__init__(
+            op_state_model,
+            obs_state_model,
+            capability_types,
+            logger,
+            _component=_component
+            or self._Component(capability_types),
+        )
+
+    @property
+    @check_communicating
+    def config_id(self):
+        return self._component.config_id
+
+    @property
+    @check_on
+    def scan_id(self):
+        return self._component.scan_id
diff --git a/src/ska_tango_base/csp_subelement_subarray.py b/src/ska_tango_base/csp/subarray/subarray_device.py
similarity index 86%
rename from src/ska_tango_base/csp_subelement_subarray.py
rename to src/ska_tango_base/csp/subarray/subarray_device.py
index 541cc8d40cc01ebf965e19bacc8d06071b63a97d..d1a6810a342405e2b0ea0f392ad46f624162b221 100644
--- a/src/ska_tango_base/csp_subelement_subarray.py
+++ b/src/ska_tango_base/csp/subarray/subarray_device.py
@@ -16,19 +16,15 @@ import json
 from json.decoder import JSONDecodeError
 from collections import defaultdict
 # Tango imports
-import tango
 from tango import DebugIt
 from tango.server import run
-from tango.server import Device
 from tango.server import attribute, command
-from tango.server import device_property
-from tango import AttrQuality, DispLevel, DevState
-from tango import AttrWriteType, PipeWriteType
+from tango import AttrWriteType
 
 # SKA import
 from ska_tango_base import SKASubarray
-from ska_tango_base.commands import ResultCode, ActionCommand
-from ska_tango_base.control_model import ObsState
+from ska_tango_base.commands import CompletionCommand, ObservationCommand, ResponseCommand, ResultCode
+from ska_tango_base.csp.subarray import CspSubarrayComponentManager
 # Additional import
 # PROTECTED REGION END #    //  CspSubElementSubarray.additionnal_import
 
@@ -187,12 +183,16 @@ class CspSubElementSubarray(SKASubarray):
     # General methods
     # ---------------
 
+    def create_component_manager(self):
+        return CspSubarrayComponentManager(self.op_state_model, self.obs_state_model)
+
     def init_command_objects(self):
         """
         Sets up the command objects
         """
         super().init_command_objects()
-        device_args = (self, self.state_model, self.logger)
+
+        device_args = (self.component_manager, self.op_state_model, self.obs_state_model, self.logger)
         self.register_command_object(
             "ConfigureScan", self.ConfigureScanCommand(*device_args)
         )
@@ -224,6 +224,8 @@ class CspSubElementSubarray(SKASubarray):
             device._sdp_output_data_rate = 0.
 
             device._config_id = ''
+
+            # JSON string, deliberately left in Tango layer
             device._last_scan_configuration = ''
 
             # _list_of_devices_completed_task: for each task/command reports
@@ -269,7 +271,7 @@ class CspSubElementSubarray(SKASubarray):
             return (ResultCode.OK, message)
 
     def always_executed_hook(self):
-        """Method always executed before any TANGO command is executed."""
+        """Method always executed before any Tango command is executed."""
         # PROTECTED REGION ID(CspSubElementSubarray.always_executed_hook) ENABLED START #
         # PROTECTED REGION END #    //  CspSubElementSubarray.always_executed_hook
 
@@ -290,13 +292,13 @@ class CspSubElementSubarray(SKASubarray):
     def read_scanID(self):
         # PROTECTED REGION ID(CspSubElementSubarray.scanID_read) ENABLED START #
         """Return the scanID attribute."""
-        return self._scan_id
+        return self.component_manager.scan_id  #pylint: disable=no-member
         # PROTECTED REGION END #    //  CspSubElementSubarray.scanID_read
 
     def read_configurationID(self):
         # PROTECTED REGION ID(CspSubElementSubarray.configurationID_read) ENABLED START #
         """Return the configurationID attribute."""
-        return self._config_id
+        return self.component_manager.config_id  #pylint: disable=no-member
         # PROTECTED REGION END #    //  CspSubElementSubarray.configurationID_read
 
     def read_sdpDestinationAddresses(self):
@@ -412,50 +414,48 @@ class CspSubElementSubarray(SKASubarray):
     # Commands
     # --------
 
-    class ConfigureScanCommand(ActionCommand):
+    class ConfigureScanCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for the CspSubElementObsDevices's ConfigureScan command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ConfigureScanCommand
 
-            :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+            :param target: the object that this base command acts upon. For
+                example, the device's component manager.
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "configure", start_action=True, logger=logger
+                target, obs_state_model, "configure", op_state_model, logger=logger
             )
 
         def do(self, argin):
             """
             Stateless hook for ConfigureScan() command functionality.
 
-            :param argin: The configuration as JSON formatted string
-            :type argin: str
+            :param argin: The configuration
+            :type argin: dict
 
             :return: A tuple containing a return code and a string
                 message indicating status. The message is for
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-            result_code, msg = self.validate_input(argin)
-            if result_code == ResultCode.FAILED:
-                return (result_code, msg)
-            # store the configuration on command success
-            device._last_scan_configuration = argin
+            component_manager = self.target
+            component_manager.configure(argin)
             return (ResultCode.OK, "Configure command completed OK")
 
         def validate_input(self, argin):
@@ -466,43 +466,46 @@ class CspSubElementSubarray(SKASubarray):
             :return: A tuple containing a return code and a string message.
             :rtype: (ResultCode, str)
             """
-            device = self.target
             try:
                 configuration_dict = json.loads(argin)
-                device._config_id = configuration_dict['id']
-                return (ResultCode.OK, "Configuration validated with success")
+                _ = configuration_dict["id"]
             except (KeyError, JSONDecodeError) as err:
-                msg = "Validate configuration failed with error:{}".format(err)
+                msg = f"Validate configuration failed with error:{err}"
+                self.logger.error(msg)
+                return (None, ResultCode.FAILED, msg)
             except Exception as other_errs:
-                msg = "Validate configuration failed with unknown error:{}".format(
-                    other_errs)
-            self.logger.error(msg)
-            return (ResultCode.FAILED, msg)
+                msg = f"Validate configuration failed with unknown error:{other_errs}"
+                self.logger.error(msg)
+                return (None, ResultCode.FAILED, msg)
 
-    class GoToIdleCommand(ActionCommand):
+            return (configuration_dict, ResultCode.OK, "ConfigureScan arguments validation successful")
+
+    class GoToIdleCommand(ObservationCommand, ResponseCommand):
         """
         A class for the CspSubElementObsDevices's GoToIdle command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
-            Constructor for GoToIdle Command.
+            Constructor for EndCommand
 
-            :param target: the object that this command acts upon; for
-                example, the CspSubElementObsDevice device for which this class
-                implements the command
+            :param target: the object that this base command acts upon. For
+                example, the device's component manager.
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "end", logger=logger
+                target, obs_state_model, "end", op_state_model, logger=logger
             )
 
         def do(self):
@@ -514,10 +517,8 @@ class CspSubElementSubarray(SKASubarray):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-            # reset to default values the configurationID and scanID
-            device._config_id = ''
-            device._scan_id = 0
+            component_manager = self.target
+            component_manager.deconfigure()
             return (ResultCode.OK, "GoToIdle command completed OK")
 
     @command(
@@ -542,8 +543,14 @@ class CspSubElementSubarray(SKASubarray):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("ConfigureScan")
-        (return_code, message) = command(argin)
-        return [[return_code], [message]]
+
+        (configuration, result_code, message) = command.validate_input(argin)
+        if result_code == ResultCode.OK:
+            # store the configuration on command success
+            self._last_scan_configuration = argin
+            (result_code, message) = command(configuration)
+
+        return [[result_code], [message]]
         # PROTECTED REGION END #    //  CspSubElementSubarray.Configure
 
     @command(
@@ -560,13 +567,13 @@ class CspSubElementSubarray(SKASubarray):
         Redirect to ConfigureScan method.
         Configure a complete scan for the subarray.
 
+        :param argin: JSON configuration string
+
         :return:'DevVarLongStringArray'
             A tuple containing a return code and a string message indicating status.
             The message is for information purpose only.
         """
-        command = self.get_command_object("ConfigureScan")
-        (return_code, message) = command(argin)
-        return [[return_code], [message]]
+        return self.ConfigureScan(argin)
         # PROTECTED REGION END #    //  CspSubElementSubarray.Configure
 
     @command(
@@ -584,6 +591,8 @@ class CspSubElementSubarray(SKASubarray):
             A tuple containing a return code and a string  message indicating status.
             The message is for information purpose only.
         """
+        self._last_scan_configuration = ''
+
         command = self.get_command_object("GoToIdle")
         (return_code, message) = command()
         return [[return_code], [message]]
@@ -604,9 +613,7 @@ class CspSubElementSubarray(SKASubarray):
             A tuple containing a return code and a string  message indicating status.
             The message is for information purpose only.
         """
-        command = self.get_command_object("GoToIdle")
-        (return_code, message) = command()
-        return [[return_code], [message]]
+        return self.GoToIdle()
         # PROTECTED REGION END #    //  CspSubElementSubarray.End
 
 # ----------
diff --git a/src/ska_tango_base/csp_subelement_state_machine.py b/src/ska_tango_base/csp_subelement_state_machine.py
deleted file mode 100644
index f2b8c216c130eed5069f0b150d639c18ccd77ec8..0000000000000000000000000000000000000000
--- a/src/ska_tango_base/csp_subelement_state_machine.py
+++ /dev/null
@@ -1,145 +0,0 @@
-"""
-This module contains specifications of the CSP SubElement Observing state machine.
-"""
-from transitions import State
-from transitions.extensions import LockedMachine as Machine
-
-__all__ = ["CspSubElementObsDeviceStateMachine"]
-
-
-class CspSubElementObsDeviceStateMachine(Machine):
-    """
-    The observation state machine used by a generic CSP 
-    Sub-element ObsDevice (derived from SKAObsDevice).
-    """
-
-    def __init__(self, callback=None, **extra_kwargs):
-        """
-        Initialises the model.
-
-        :param callback: A callback to be called when the state changes
-        :type callback: callable
-        :param extra_kwargs: Additional keywords arguments to pass to super class
-            initialiser (useful for graphing)
-        """
-        self._callback = callback
-
-        states = [
-            "IDLE",
-            "CONFIGURING",
-            "READY",
-            "SCANNING",
-            "ABORTING",
-            "ABORTED",
-            "FAULT",
-        ]
-        transitions = [
-            {
-                "source": "*",
-                "trigger": "fatal_error",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["IDLE", "READY"],
-                "trigger": "configure_started",
-                "dest": "CONFIGURING",
-            },
-            {
-                "source": "CONFIGURING",
-                "trigger": "configure_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "CONFIGURING",
-                "trigger": "configure_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "READY",
-                "trigger": "end_succeeded",
-                "dest": "IDLE",
-            },
-            {
-                "source": "IDLE",
-                "trigger": "end_succeeded",
-                "dest": "IDLE",
-            },
-            {
-                "source": "READY",
-                "trigger": "end_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "READY",
-                "trigger": "scan_started",
-                "dest": "SCANNING",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "scan_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "scan_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "end_scan_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "end_scan_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": [
-                    "CONFIGURING",
-                    "READY",
-                    "SCANNING",
-                    "IDLE",
-                ],
-                "trigger": "abort_started",
-                "dest": "ABORTING",
-            },
-            {
-                "source": "ABORTING",
-                "trigger": "abort_succeeded",
-                "dest": "ABORTED",
-            },
-            {
-                "source": "ABORTING",
-                "trigger": "abort_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["ABORTED", "FAULT"],
-                "trigger": "reset_succeeded",
-                "dest": "IDLE",
-            },
-            {
-                "source": ["ABORTED", "FAULT"],
-                "trigger": "reset_failed",
-                "dest": "FAULT",
-            },
-        ]
-
-        super().__init__(
-            states=states,
-            initial="IDLE",
-            transitions=transitions,
-            after_state_change=self._state_changed,
-            **extra_kwargs,
-        )
-        self._state_changed()
-
-    def _state_changed(self):
-        """
-        State machine callback that is called every time the obs_state
-        changes. Responsible for ensuring that callbacks are called.
-        """
-        if self._callback is not None:
-            self._callback(self.state)
-
diff --git a/src/ska_tango_base/faults.py b/src/ska_tango_base/faults.py
index b25218b6ad9a3b7d2b76ad024c22cfd8236b1730..516a561a1732965c059c2b169a13e7ea25104b34 100644
--- a/src/ska_tango_base/faults.py
+++ b/src/ska_tango_base/faults.py
@@ -31,3 +31,9 @@ class CommandError(RuntimeError):
 
 class CapabilityValidationError(ValueError):
     """Error in validating capability input against capability types."""
+
+class ComponentError(Exception):
+    """Component cannot perform as requested."""
+
+class ComponentFault(ComponentError):
+    """Component is in FAULT state and cannot perform as requested."""
diff --git a/src/ska_tango_base/logger_device.py b/src/ska_tango_base/logger_device.py
index 17166b970b0da36a4fe63fc4808f6cf96d3823c9..5c2bc5f26a657762c5931c19311ff267c785eda9 100644
--- a/src/ska_tango_base/logger_device.py
+++ b/src/ska_tango_base/logger_device.py
@@ -6,7 +6,7 @@
 #
 """
 This module implements SKALogger device, a generic base device for
-logging for SKA. It enables to view on-line logs through the TANGO
+logging for SKA. It enables to view on-line logs through the Tango
 Logging Services and to store logs using Python logging. It configures
 the log levels of remote logging for selected devices.
 """
@@ -49,7 +49,7 @@ class SKALogger(SKABaseDevice):
         super().init_command_objects()
         self.register_command_object(
             "SetLoggingLevel",
-            self.SetLoggingLevelCommand(self, self.state_model, self.logger)
+            self.SetLoggingLevelCommand(self, self.op_state_model, self.logger)
         )
 
     def always_executed_hook(self):
@@ -78,9 +78,8 @@ class SKALogger(SKABaseDevice):
             """
             Constructor for SetLoggingLevelCommand
 
-            :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+            :param target: the object that this base command acts upon. For
+                example, the device's component manager.
             :type target: object
             :param state_model: the state model that this command uses
                  to check that it is allowed to run, and that it drives
diff --git a/src/ska_tango_base/master_device.py b/src/ska_tango_base/master_device.py
index 0fb64d15716ad97bb76ab66f34b11774bfbcf464..741d409c9d8668628e5effeae4e5a7230e557b4b 100644
--- a/src/ska_tango_base/master_device.py
+++ b/src/ska_tango_base/master_device.py
@@ -38,7 +38,7 @@ class SKAMaster(SKABaseDevice):
         self.register_command_object(
             "IsCapabilityAchievable",
             self.IsCapabilityAchievableCommand(
-                self, self.state_model, self.logger
+                self, self.op_state_model, self.logger
             )
         )
 
diff --git a/src/ska_tango_base/obs/__init__.py b/src/ska_tango_base/obs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..be70fa3d4a0447df6930be8725120395e1e7b3d0
--- /dev/null
+++ b/src/ska_tango_base/obs/__init__.py
@@ -0,0 +1,13 @@
+"""
+This subpackage models a SKA Tango observing device.
+"""
+
+__all__ = (
+    "ObsStateModel",
+    "SKAObsDevice",
+)
+
+# Note: order of imports is important - start with lowest in the hierarchy
+from .obs_state_model import ObsStateModel
+
+from .obs_device import SKAObsDevice
diff --git a/src/ska_tango_base/obs/obs_device.py b/src/ska_tango_base/obs/obs_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..31d70fdb23f8ce5defcad3800798ab9c7d48efa0
--- /dev/null
+++ b/src/ska_tango_base/obs/obs_device.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the SKAObsDevice project
+#
+#
+#
+""" SKAObsDevice
+
+A generic base device for Observations for SKA. It inherits SKABaseDevice
+class. Any device implementing an obsMode will inherit from SKAObsDevice
+instead of just SKABaseDevice.
+"""
+
+# Additional import
+# PROTECTED REGION ID(SKAObsDevice.additionnal_import) ENABLED START #
+import warnings
+
+# Tango imports
+from tango.server import run, attribute
+
+# SKA specific imports
+from ska_tango_base import SKABaseDevice
+from ska_tango_base.commands import ResultCode
+from ska_tango_base.control_model import ObsMode, ObsState
+# PROTECTED REGION END #    //  SKAObsDevice.additionnal_imports
+
+__all__ = ["SKAObsDevice", "main"]
+
+
+class SKAObsDevice(SKABaseDevice):
+    """
+    A generic base device for Observations for SKA.
+    """
+    class InitCommand(SKABaseDevice.InitCommand):
+        """
+        A class for the SKAObsDevice's init_device() "command".
+        """
+
+        def do(self):
+            """
+            Stateless hook for device initialisation.
+
+            :return: A tuple containing a return code and a string
+                message indicating status. The message is for
+                information purpose only.
+            :rtype: (ResultCode, str)
+            """
+            super().do()
+
+            device = self.target
+            device.set_change_event("obsState", True, True)
+            device.set_archive_event("obsState", True, True)
+
+            device._obs_state = ObsState.EMPTY
+            device._obs_mode = ObsMode.IDLE
+            device._config_progress = 0
+            device._config_delay_expected = 0
+
+            message = "SKAObsDevice Init command completed OK"
+            self.logger.info(message)
+            return (ResultCode.OK, message)
+
+    # PROTECTED REGION ID(SKAObsDevice.class_variable) ENABLED START #
+
+    # PROTECTED REGION END #    //  SKAObsDevice.class_variable
+
+    # -----------------
+    # Device Properties
+    # -----------------
+
+    # ----------
+    # Attributes
+    # ----------
+
+    obsState = attribute(
+        dtype=ObsState,
+        doc="Observing State",
+    )
+    """Device attribute."""
+
+    obsMode = attribute(
+        dtype=ObsMode,
+        doc="Observing Mode",
+    )
+    """Device attribute."""
+
+    configurationProgress = attribute(
+        dtype='uint16',
+        unit="%",
+        max_value=100,
+        min_value=0,
+        doc="Percentage configuration progress",
+    )
+    """Device attribute."""
+
+    configurationDelayExpected = attribute(
+        dtype='uint16',
+        unit="seconds",
+        doc="Configuration delay expected in seconds",
+    )
+    """Device attribute."""
+
+    # ---------------
+    # General methods
+    # ---------------
+    def _update_obs_state(self, obs_state):
+        """
+        Helper method for changing obs_state; passed to the state model as a
+        callback
+
+        :param obs_state: the new obs_state value
+        :type obs_state: :py:class:`~ska_tango_base.control_model.ObsState`
+        """
+        self._obs_state = obs_state
+        self.push_change_event("obsState", obs_state)
+        self.push_archive_event("obsState", obs_state)
+
+    def always_executed_hook(self):
+        # PROTECTED REGION ID(SKAObsDevice.always_executed_hook) ENABLED START #
+        """
+        Method that is always executed before any device command gets executed.
+
+        :return: None
+        """
+        pass
+        # PROTECTED REGION END #    //  SKAObsDevice.always_executed_hook
+
+    def delete_device(self):
+        # PROTECTED REGION ID(SKAObsDevice.delete_device) ENABLED START #
+        """
+        Method to cleanup when device is stopped.
+
+        :return: None
+        """
+        pass
+        # PROTECTED REGION END #    //  SKAObsDevice.delete_device
+
+    # ------------------
+    # Attributes methods
+    # ------------------
+
+    def read_obsState(self):
+        # PROTECTED REGION ID(SKAObsDevice.obsState_read) ENABLED START #
+        """Reads Observation State of the device"""
+        return self._obs_state
+        # PROTECTED REGION END #    //  SKAObsDevice.obsState_read
+
+    def read_obsMode(self):
+        # PROTECTED REGION ID(SKAObsDevice.obsMode_read) ENABLED START #
+        """Reads Observation Mode of the device"""
+        return self._obs_mode
+        # PROTECTED REGION END #    //  SKAObsDevice.obsMode_read
+
+    def read_configurationProgress(self):
+        # PROTECTED REGION ID(SKAObsDevice.configurationProgress_read) ENABLED START #
+        """Reads percentage configuration progress of the device"""
+        return self._config_progress
+        # PROTECTED REGION END #    //  SKAObsDevice.configurationProgress_read
+
+    def read_configurationDelayExpected(self):
+        # PROTECTED REGION ID(SKAObsDevice.configurationDelayExpected_read) ENABLED START #
+        """Reads expected Configuration Delay in seconds"""
+        return self._config_delay_expected
+        # PROTECTED REGION END #    //  SKAObsDevice.configurationDelayExpected_read
+
+    # --------
+    # Commands
+    # --------
+
+# ----------
+# Run server
+# ----------
+
+
+def main(args=None, **kwargs):
+    # PROTECTED REGION ID(SKAObsDevice.main) ENABLED START #
+    """
+    Main function of the SKAObsDevice module.
+
+    :param args: positional arguments
+    :param kwargs: keyword arguments
+    """
+    return run((SKAObsDevice,), args=args, **kwargs)
+    # PROTECTED REGION END #    //  SKAObsDevice.main
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/ska_tango_base/obs/obs_state_model.py b/src/ska_tango_base/obs/obs_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..c47dc0e2b3c9bbe268e870f373a3ff6109f8978f
--- /dev/null
+++ b/src/ska_tango_base/obs/obs_state_model.py
@@ -0,0 +1,147 @@
+"""
+This module defines a basic state model for SKA LMC devices that manage
+observations. It consists of a single :py:class:`.ObsStateModel` class,
+which drives a state machine to manage device "obs state", represented
+by the :py:class:`ska_tango_base.control_model.ObsState` enum, and
+published by Tango devices via the ``obsState`` attribute.
+"""
+from ska_tango_base.control_model import ObsState
+from ska_tango_base.faults import StateModelError
+from ska_tango_base.utils import for_testing_only
+
+
+__all__ = ["ObsStateModel"]
+
+
+class ObsStateModel:
+    """
+    This class implements the state model for observation state
+    ("obsState").
+
+    The model supports states that are values of the
+    :py:class:`ska_tango_base.control_model.ObsState` enum. Rather than
+    specifying a state machine, it allows a state machine to be
+    provided. Thus, the precise states supported, and the transitions,
+    are not determined in advance.
+    """
+
+    def __init__(
+        self,
+        state_machine_factory,
+        logger,
+        callback=None,
+    ):
+        """
+        Initialises the model.
+
+        :param state_machine_factory: a callable that returns a
+            state machine for this model to use
+        :type state_machine_factory: callable
+        :param logger: the logger to be used by this state model.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param callback: A callback to be called when a state machine
+            transitions state
+        :type callback: callable
+        """
+        self.logger = logger
+
+        self._obs_state = None
+        self._callback = callback
+
+        self._obs_state_machine = state_machine_factory(
+            callback=self._obs_state_changed
+        )
+
+    @property
+    def obs_state(self):
+        """
+        Returns the obs_state
+
+        :returns: obs_state of this state model
+        :rtype: ObsState
+        """
+        return self._obs_state
+
+    def _obs_state_changed(self, machine_state):
+        """
+        Helper method that updates obs_state whenever the observation
+        state machine reports a change of state, ensuring that the
+        callback is called if one exists.
+
+        :param machine_state: the new state of the observation state
+            machine
+        :type machine_state: str
+        """
+        obs_state = ObsState[machine_state]
+        if self._obs_state != obs_state:
+            self._obs_state = obs_state
+            if self._callback is not None:
+                self._callback(obs_state)
+
+    def is_action_allowed(self, action, raise_if_disallowed=False):
+        """
+        Whether a given action is allowed in the current state.
+
+        :param action: an action, as given in the transitions table
+        :type action: str
+
+        :param raise_if_disallowed: whether to raise an exception if the
+            action is disallowed, or merely return False (optional,
+            defaults to False)
+        :type raise_if_disallowed: bool
+
+        :raises StateModelError: if the action is unknown to the state
+            machine
+
+        :return: whether the action is allowed in the current state
+        :rtype: bool
+        """
+        if action in self._obs_state_machine.get_triggers(
+            self._obs_state_machine.state
+        ):
+            return True
+
+        if raise_if_disallowed:
+            raise StateModelError(
+                f"Action {action} is not allowed in obs state {self.obs_state.name}."
+            )
+        return False
+
+    def perform_action(self, action):
+        """
+        Performs an action on the state model
+
+        :param action: an action, as given in the transitions table
+        :type action: ANY
+        """
+        _ = self.is_action_allowed(action, raise_if_disallowed=True)
+        self._obs_state_machine.trigger(action)
+
+    @for_testing_only
+    def _straight_to_state(self, obs_state_name):
+        """
+        Takes this model straight to the specified state. This method
+        exists to simplify testing; for example, if testing that a
+        command may be run in a given ObsState, one can push this state
+        model straight to that ObsState, rather than having to drive it
+        to that state through a sequence of actions. It is not intended
+        that this method would be called outside of test setups. A
+        warning will be raised if it is.
+
+        For example, to test that a device transitions from SCANNING to
+        ABORTING when the Abort() command is called:
+
+        .. code-block:: py
+
+          model = ObservationStateModel(logger)
+          model._straight_to_state("SCANNING")
+          assert model.obs_state == ObsState.SCANNING
+          model.perform_action("abort_invoked")
+          assert model.obs_state == ObsState.ABORTING
+
+        :param obs_state_name: the target obs_state
+        :type obs_state_name:
+            :py:class:`~ska_tango_base.control_model.ObsState`
+        """
+        getattr(self._obs_state_machine, f"to_{obs_state_name}")()
diff --git a/src/ska_tango_base/obs_device.py b/src/ska_tango_base/obs_device.py
deleted file mode 100644
index b275530ecc6dfbbbfa0c457c5ba8445f80eb0dc6..0000000000000000000000000000000000000000
--- a/src/ska_tango_base/obs_device.py
+++ /dev/null
@@ -1,385 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of the SKAObsDevice project
-#
-#
-#
-""" SKAObsDevice
-
-A generic base device for Observations for SKA. It inherits SKABaseDevice
-class. Any device implementing an obsMode will inherit from SKAObsDevice
-instead of just SKABaseDevice.
-"""
-
-# Additional import
-# PROTECTED REGION ID(SKAObsDevice.additionnal_import) ENABLED START #
-import warnings
-
-# Tango imports
-from tango import DevState
-from tango.server import run, attribute
-
-# SKA specific imports
-from ska_tango_base import SKABaseDevice, DeviceStateModel
-from ska_tango_base.commands import ResultCode
-from ska_tango_base.control_model import ObsMode, ObsState
-from ska_tango_base.faults import StateModelError
-from ska_tango_base.utils import for_testing_only
-# PROTECTED REGION END #    //  SKAObsDevice.additionnal_imports
-
-__all__ = ["SKAObsDevice", "ObsDeviceStateModel", "main"]
-
-
-class ObsDeviceStateModel(DeviceStateModel):
-    """
-    Base class for ObsDevice state models
-    """
-
-    def __init__(
-        self,
-        action_breakdown,
-        obs_machine_class,
-        logger,
-        op_state_callback=None,
-        admin_mode_callback=None,
-        obs_state_callback=None,
-    ):
-        """
-        Initialises the model.
-
-        :param action_breakdown: the action breakdown associated with the observing
-           state machine class
-        :type action_breakdown: dictionary defining actions to be performed
-            on the observation state machine and,as needed, on the device state machine.
-        :param obs_machine_class: state machine for the observing state of a
-            SKAObsDevice class device.
-        :type obs_machine_class: :py:class:`transitions.Machine`
-        :param logger: the logger to be used by this state model.
-        :type logger: a logger that implements the standard library
-            logger interface
-        :param op_state_callback: A callback to be called when a
-            transition implies a change to op state
-        :type op_state_callback: callable
-        :param admin_mode_callback: A callback to be called when a
-            transition causes a change to device admin_mode
-        :type admin_mode_callback: callable
-        :param obs_state_callback: A callback to be called when a
-            transition causes a change to device obs_state
-        :type obs_state_callback: callable
-        """
-        super().__init__(
-            logger,
-            op_state_callback=op_state_callback,
-            admin_mode_callback=admin_mode_callback,
-        )
-
-        self._obs_state = None
-        self._obs_state_callback = obs_state_callback
-
-        self._observation_state_machine = obs_machine_class(
-            self._update_obs_state
-        )
-        self._action_breakdown = dict(action_breakdown)
-
-    @property
-    def obs_state(self):
-        """
-        Returns the obs_state
-
-        :returns: obs_state of this state model
-        :rtype: ObsState
-        """
-        return self._obs_state
-
-    def _update_obs_state(self, machine_state):
-        """
-        Helper method that updates obs_state whenever the observation
-        state machine reports a change of state, ensuring that the
-        callback is called if one exists.
-
-        :param machine_state: the new state of the observation state
-            machine
-        :type machine_state: str
-        """
-        obs_state = ObsState[machine_state]
-        if self._obs_state != obs_state:
-            self._obs_state = obs_state
-            if self._obs_state_callback is not None:
-                self._obs_state_callback(obs_state)
-
-    def _is_obs_action_allowed(self, action):
-        if action not in self._action_breakdown:
-            return None
-
-        if self.op_state != DevState.ON:
-            return False
-
-        (obs_action, super_action) = self._action_breakdown[action]
-
-        if obs_action not in self._observation_state_machine.get_triggers(
-            self._observation_state_machine.state
-        ):
-            return False
-        return super_action is None or super().is_action_allowed(super_action)
-
-    def is_action_allowed(self, action):
-        """
-        Whether a given action is allowed in the current state.
-
-        :param action: an action, as given in the transitions table
-        :type action: ANY
-
-        :returns: where the action is allowed in the current state:
-        :rtype: bool: True if the action is allowed, False if it is
-            not allowed
-        :raises StateModelError: for an unrecognised action
-        """
-        obs_allowed = self._is_obs_action_allowed(action)
-        if obs_allowed is None:
-            return super().is_action_allowed(action)
-        if obs_allowed:
-            return True
-        try:
-            return super().is_action_allowed(action)
-        except StateModelError:
-            return False
-
-    def try_action(self, action):
-        """
-        Checks whether a given action is allowed in the current state,
-        and raises a StateModelError if it is not.
-
-        :param action: an action, as given in the transitions table
-        :type action: str
-
-        :raises StateModelError: if the action is not allowed in the
-            current state
-
-        :returns: True if the action is allowed
-        :rtype: boolean
-        """
-        if not self.is_action_allowed(action):
-            raise StateModelError(
-                f"Action {action} is not allowed in operational state "
-                f"{self.op_state}, admin mode {self.admin_mode}, "
-                f"observation state {self.obs_state}."
-            )
-        return True
-
-    def perform_action(self, action):
-        """
-        Performs an action on the state model
-
-        :param action: an action, as given in the transitions table
-        :type action: ANY
-
-        :raises StateModelError: if the action is not allowed in the
-            current state
-
-        """
-        self.try_action(action)
-
-        if self._is_obs_action_allowed(action):
-            (obs_action, super_action) = self._action_breakdown[action]
-
-            if obs_action.startswith("to_"):
-                message = (
-                    f"Forcing device state of an observing device "
-                    f"should only be done as an emergency measure and may be "
-                    f"disallowed in future (action: {obs_action})."
-                )
-                self.logger.warning(message)
-                warnings.warn(message, PendingDeprecationWarning)
-
-            self._observation_state_machine.trigger(obs_action)
-            if super_action is not None:
-                super().perform_action(super_action)
-        else:
-            super().perform_action(action)
-
-    @for_testing_only
-    def _straight_to_state(self, op_state=None, admin_mode=None, obs_state=None):
-        """
-        Takes the ObsDeviceStateModel straight to the specified states.
-        This method exists to simplify testing; for example, if testing
-        that a command may be run in a given state, one can push the
-        state model straight to that state, rather than having to drive
-        it to that state through a sequence of actions. It is not
-        intended that this method would be called outside of test
-        setups. A warning will be raised if it is.
-
-        Note that this method will allow you to put the device into an
-        incoherent combination of states and modes (e.g. adminMode
-        OFFLINE, opState STANDBY, and obsState SCANNING).
-
-        :param op_state: the target operational state (optional)
-        :type op_state: :py:class:`tango.DevState`
-        :param admin_mode: the target admin mode (optional)
-        :type admin_mode: :py:class:`~ska_tango_base.control_model.AdminMode`
-        :param obs_state: the target observation state (optional)
-        :type obs_state: :py:class:`~ska_tango_base.control_model.ObsState`
-        """
-        if obs_state is not None:
-            getattr(self._observation_state_machine, f"to_{obs_state.name}")()
-        super()._straight_to_state(op_state=op_state, admin_mode=admin_mode)
-
-
-class SKAObsDevice(SKABaseDevice):
-    """
-    A generic base device for Observations for SKA.
-    """
-    class InitCommand(SKABaseDevice.InitCommand):
-        """
-        A class for the SKAObsDevice's init_device() "command".
-        """
-
-        def do(self):
-            """
-            Stateless hook for device initialisation.
-
-            :return: A tuple containing a return code and a string
-                message indicating status. The message is for
-                information purpose only.
-            :rtype: (ResultCode, str)
-            """
-            super().do()
-
-            device = self.target
-            device.set_change_event("obsState", True, True)
-            device.set_archive_event("obsState", True, True)
-
-            device._obs_state = ObsState.EMPTY
-            device._obs_mode = ObsMode.IDLE
-            device._config_progress = 0
-            device._config_delay_expected = 0
-
-            message = "SKAObsDevice Init command completed OK"
-            self.logger.info(message)
-            return (ResultCode.OK, message)
-
-    # PROTECTED REGION ID(SKAObsDevice.class_variable) ENABLED START #
-
-    # PROTECTED REGION END #    //  SKAObsDevice.class_variable
-
-    # -----------------
-    # Device Properties
-    # -----------------
-
-    # ----------
-    # Attributes
-    # ----------
-
-    obsState = attribute(
-        dtype=ObsState,
-        doc="Observing State",
-    )
-    """Device attribute."""
-
-    obsMode = attribute(
-        dtype=ObsMode,
-        doc="Observing Mode",
-    )
-    """Device attribute."""
-
-    configurationProgress = attribute(
-        dtype='uint16',
-        unit="%",
-        max_value=100,
-        min_value=0,
-        doc="Percentage configuration progress",
-    )
-    """Device attribute."""
-
-    configurationDelayExpected = attribute(
-        dtype='uint16',
-        unit="seconds",
-        doc="Configuration delay expected in seconds",
-    )
-    """Device attribute."""
-
-    # ---------------
-    # General methods
-    # ---------------
-    def _update_obs_state(self, obs_state):
-        """
-        Helper method for changing obs_state; passed to the state model as a
-        callback
-
-        :param obs_state: the new obs_state value
-        :type admin_mode: :py:class:`~ska_tango_base.control_model.ObsState`
-        """
-        self._obs_state = obs_state
-        self.push_change_event("obsState", obs_state)
-        self.push_archive_event("obsState", obs_state)
-
-    def always_executed_hook(self):
-        # PROTECTED REGION ID(SKAObsDevice.always_executed_hook) ENABLED START #
-        """
-        Method that is always executed before any device command gets executed.
-
-        :return: None
-        """
-        pass
-        # PROTECTED REGION END #    //  SKAObsDevice.always_executed_hook
-
-    def delete_device(self):
-        # PROTECTED REGION ID(SKAObsDevice.delete_device) ENABLED START #
-        """
-        Method to cleanup when device is stopped.
-
-        :return: None
-        """
-        pass
-        # PROTECTED REGION END #    //  SKAObsDevice.delete_device
-
-    # ------------------
-    # Attributes methods
-    # ------------------
-
-    def read_obsState(self):
-        # PROTECTED REGION ID(SKAObsDevice.obsState_read) ENABLED START #
-        """Reads Observation State of the device"""
-        return self._obs_state
-        # PROTECTED REGION END #    //  SKAObsDevice.obsState_read
-
-    def read_obsMode(self):
-        # PROTECTED REGION ID(SKAObsDevice.obsMode_read) ENABLED START #
-        """Reads Observation Mode of the device"""
-        return self._obs_mode
-        # PROTECTED REGION END #    //  SKAObsDevice.obsMode_read
-
-    def read_configurationProgress(self):
-        # PROTECTED REGION ID(SKAObsDevice.configurationProgress_read) ENABLED START #
-        """Reads percentage configuration progress of the device"""
-        return self._config_progress
-        # PROTECTED REGION END #    //  SKAObsDevice.configurationProgress_read
-
-    def read_configurationDelayExpected(self):
-        # PROTECTED REGION ID(SKAObsDevice.configurationDelayExpected_read) ENABLED START #
-        """Reads expected Configuration Delay in seconds"""
-        return self._config_delay_expected
-        # PROTECTED REGION END #    //  SKAObsDevice.configurationDelayExpected_read
-
-    # --------
-    # Commands
-    # --------
-
-# ----------
-# Run server
-# ----------
-
-
-def main(args=None, **kwargs):
-    # PROTECTED REGION ID(SKAObsDevice.main) ENABLED START #
-    """
-    Main function of the SKAObsDevice module.
-
-    :param args: None
-    :param kwargs:
-    """
-    return run((SKAObsDevice,), args=args, **kwargs)
-    # PROTECTED REGION END #    //  SKAObsDevice.main
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/ska_tango_base/release.py b/src/ska_tango_base/release.py
index 8b8714e48dde3db439881c484a47a372068ea95e..3c3cc0aa4a425aba06fc35ad234eab21f81c1a32 100644
--- a/src/ska_tango_base/release.py
+++ b/src/ska_tango_base/release.py
@@ -7,7 +7,7 @@
 """Release information for ska_tango_base Python Package"""
 
 name = """ska_tango_base"""
-version = "0.10.1"
+version = "0.11.0"
 version_info = version.split(".")
 description = """A set of generic base devices for SKA Telescope."""
 author = "SKA India and SARAO and CSIRO and INAF"
diff --git a/src/ska_tango_base/state_machine.py b/src/ska_tango_base/state_machine.py
deleted file mode 100644
index 5c5104a3646ae21f0929365355bc9e7c64d4d353..0000000000000000000000000000000000000000
--- a/src/ska_tango_base/state_machine.py
+++ /dev/null
@@ -1,467 +0,0 @@
-"""
-This module contains specifications of SKA state machines.
-"""
-from transitions import State
-from transitions.extensions import LockedMachine as Machine
-
-
-__all__ = ["OperationStateMachine", "AdminModeStateMachine", "ObservationStateMachine"]
-
-
-class OperationStateMachine(Machine):
-    """
-    State machine for operational state ("opState").
-
-    The states supported are "UNINITIALISED", "INIT", "FAULT",
-    "DISABLE", "STANDBY", "OFF" and "ON".
-
-    The states "INIT", "FAULT" and "DISABLE" also have "INIT_ADMIN",
-    "FAULT_ADMIN" and "DISABLE_ADMIN" flavours to represent these states
-    in situations where the device being modelled has been
-    administratively disabled.
-    """
-
-    def __init__(self, callback=None, **extra_kwargs):
-        """
-        Initialises the state model.
-
-        :param callback: A callback to be called when a transition
-            implies a change to op state
-        :type callback: callable
-        :param extra_kwargs: Additional keywords arguments to pass to super class
-            initialiser (useful for graphing)
-        """
-        self._callback = callback
-
-        states = [
-            "UNINITIALISED",
-            "INIT",
-            "INIT_ADMIN",
-            "FAULT",
-            "FAULT_ADMIN",
-            "DISABLE",
-            "DISABLE_ADMIN",
-            "STANDBY",
-            "OFF",
-            "ON",
-        ]
-
-        transitions = [
-            {
-                "source": "UNINITIALISED",
-                "trigger": "init_started",
-                "dest": "INIT",
-            },
-            {
-                "source": ["INIT", "FAULT", "DISABLE", "STANDBY", "OFF", "ON"],
-                "trigger": "fatal_error",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["INIT_ADMIN", "FAULT_ADMIN", "DISABLE_ADMIN"],
-                "trigger": "fatal_error",
-                "dest": "FAULT_ADMIN",
-            },
-            {
-                "source": "INIT",
-                "trigger": "init_succeeded_disable",
-                "dest": "DISABLE",
-            },
-            {
-                "source": "INIT_ADMIN",
-                "trigger": "init_succeeded_disable",
-                "dest": "DISABLE_ADMIN",
-            },
-            {
-                "source": "INIT",
-                "trigger": "init_succeeded_standby",
-                "dest": "STANDBY",
-            },
-            {
-                "source": "INIT",
-                "trigger": "init_succeeded_off",
-                "dest": "OFF",
-            },
-            {
-                "source": "INIT",
-                "trigger": "init_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "INIT_ADMIN",
-                "trigger": "init_failed",
-                "dest": "FAULT_ADMIN",
-            },
-            {
-                "source": ["INIT", "INIT_ADMIN"],
-                "trigger": "admin_on",
-                "dest": "INIT_ADMIN",
-            },
-            {
-                "source": ["INIT", "INIT_ADMIN"],
-                "trigger": "admin_off",
-                "dest": "INIT",
-            },
-            {
-                "source": "FAULT",
-                "trigger": "reset_succeeded_disable",
-                "dest": "DISABLE",
-            },
-            {
-                "source": "FAULT_ADMIN",
-                "trigger": "reset_succeeded_disable",
-                "dest": "DISABLE_ADMIN",
-            },
-            {
-                "source": "FAULT",
-                "trigger": "reset_succeeded_standby",
-                "dest": "STANDBY",
-            },
-            {
-                "source": "FAULT",
-                "trigger": "reset_succeeded_off",
-                "dest": "OFF",
-            },
-            {
-                "source": "FAULT",
-                "trigger": "reset_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "FAULT_ADMIN",
-                "trigger": "reset_failed",
-                "dest": "FAULT_ADMIN",
-            },
-            {
-                "source": ["FAULT", "FAULT_ADMIN"],
-                "trigger": "admin_on",
-                "dest": "FAULT_ADMIN",
-            },
-            {
-                "source": ["FAULT", "FAULT_ADMIN"],
-                "trigger": "admin_off",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["DISABLE", "DISABLE_ADMIN"],
-                "trigger": "admin_on",
-                "dest": "DISABLE_ADMIN",
-            },
-            {
-                "source": ["DISABLE", "DISABLE_ADMIN"],
-                "trigger": "admin_off",
-                "dest": "DISABLE",
-            },
-            {
-                "source": "DISABLE_ADMIN",
-                "trigger": "disable_succeeded",
-                "dest": "DISABLE_ADMIN",
-            },
-            {
-                "source": "DISABLE_ADMIN",
-                "trigger": "disable_failed",
-                "dest": "FAULT_ADMIN",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF"],
-                "trigger": "disable_succeeded",
-                "dest": "DISABLE",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF"],
-                "trigger": "disable_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF"],
-                "trigger": "standby_succeeded",
-                "dest": "STANDBY",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF"],
-                "trigger": "standby_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF", "ON"],
-                "trigger": "off_succeeded",
-                "dest": "OFF",
-            },
-            {
-                "source": ["DISABLE", "STANDBY", "OFF", "ON"],
-                "trigger": "off_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["OFF", "ON"],
-                "trigger": "on_succeeded",
-                "dest": "ON",
-            },
-            {
-                "source": ["OFF", "ON"],
-                "trigger": "on_failed",
-                "dest": "FAULT",
-            },
-        ]
-
-        super().__init__(
-            states=states,
-            initial="UNINITIALISED",
-            transitions=transitions,
-            after_state_change=self._state_changed,
-            **extra_kwargs,
-        )
-
-    def _state_changed(self):
-        """
-        State machine callback that is called every time the op_state
-        changes. Responsible for ensuring that callbacks are called.
-        """
-        if self._callback is not None:
-            self._callback(self.state)
-
-
-class AdminModeStateMachine(Machine):
-    """
-    The state machine governing admin modes
-    """
-
-    def __init__(self, callback=None, **extra_kwargs):
-        """
-        Initialises the admin mode state machine model.
-
-        :param callback: A callback to be called whenever there is a transition
-            to a new admin mode value
-        :type callback: callable
-        :param extra_kwargs: Additional keywords arguments to pass to super class
-            initialiser (useful for graphing)
-        """
-        self._callback = callback
-
-        states = ["RESERVED", "NOT_FITTED", "OFFLINE", "MAINTENANCE", "ONLINE"]
-        transitions = [
-            {
-                "source": ["NOT_FITTED", "RESERVED", "OFFLINE"],
-                "trigger": "to_reserved",
-                "dest": "RESERVED",
-            },
-            {
-                "source": ["RESERVED", "NOT_FITTED", "OFFLINE"],
-                "trigger": "to_notfitted",
-                "dest": "NOT_FITTED",
-            },
-            {
-                "source": ["RESERVED", "NOT_FITTED", "OFFLINE", "MAINTENANCE", "ONLINE"],
-                "trigger": "to_offline",
-                "dest": "OFFLINE",
-            },
-            {
-                "source": ["OFFLINE", "MAINTENANCE", "ONLINE"],
-                "trigger": "to_maintenance",
-                "dest": "MAINTENANCE",
-            },
-            {
-                "source": ["OFFLINE", "MAINTENANCE", "ONLINE"],
-                "trigger": "to_online",
-                "dest": "ONLINE",
-            },
-        ]
-
-        super().__init__(
-            states=states,
-            initial="MAINTENANCE",
-            transitions=transitions,
-            after_state_change=self._state_changed,
-            **extra_kwargs,
-        )
-        self._state_changed()
-
-    def _state_changed(self):
-        """
-        State machine callback that is called every time the admin mode
-        changes. Responsible for ensuring that callbacks are called.
-        """
-        if self._callback is not None:
-            self._callback(self.state)
-
-
-class ObservationStateMachine(Machine):
-    """
-    The observation state machine used by an observing subarray, per
-    ADR-8.
-    """
-
-    def __init__(self, callback=None, **extra_kwargs):
-        """
-        Initialises the model.
-
-        :param callback: A callback to be called when the state changes
-        :type callback: callable
-        :param extra_kwargs: Additional keywords arguments to pass to super class
-            initialiser (useful for graphing)
-        """
-        self._callback = callback
-
-        states = [
-            "EMPTY",
-            "RESOURCING",
-            "IDLE",
-            "CONFIGURING",
-            "READY",
-            "SCANNING",
-            "ABORTING",
-            "ABORTED",
-            "RESETTING",
-            "RESTARTING",
-            "FAULT",
-        ]
-        transitions = [
-            {
-                "source": "*",
-                "trigger": "fatal_error",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["EMPTY", "IDLE"],
-                "trigger": "assign_started",
-                "dest": "RESOURCING",
-            },
-            {
-                "source": "IDLE",
-                "trigger": "release_started",
-                "dest": "RESOURCING",
-            },
-            {
-                "source": "RESOURCING",
-                "trigger": "resourcing_succeeded_some_resources",
-                "dest": "IDLE",
-            },
-            {
-                "source": "RESOURCING",
-                "trigger": "resourcing_succeeded_no_resources",
-                "dest": "EMPTY",
-            },
-            {
-                "source": "RESOURCING",
-                "trigger": "resourcing_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["IDLE", "READY"],
-                "trigger": "configure_started",
-                "dest": "CONFIGURING",
-            },
-            {
-                "source": "CONFIGURING",
-                "trigger": "configure_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "CONFIGURING",
-                "trigger": "configure_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "READY",
-                "trigger": "end_succeeded",
-                "dest": "IDLE",
-            },
-            {
-                "source": "READY",
-                "trigger": "end_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "READY",
-                "trigger": "scan_started",
-                "dest": "SCANNING",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "scan_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "scan_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "end_scan_succeeded",
-                "dest": "READY",
-            },
-            {
-                "source": "SCANNING",
-                "trigger": "end_scan_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": [
-                    "IDLE",
-                    "CONFIGURING",
-                    "READY",
-                    "SCANNING",
-                    "RESETTING",
-                ],
-                "trigger": "abort_started",
-                "dest": "ABORTING",
-            },
-            {
-                "source": "ABORTING",
-                "trigger": "abort_succeeded",
-                "dest": "ABORTED",
-            },
-            {
-                "source": "ABORTING",
-                "trigger": "abort_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["ABORTED", "FAULT"],
-                "trigger": "reset_started",
-                "dest": "RESETTING",
-            },
-            {
-                "source": "RESETTING",
-                "trigger": "reset_succeeded",
-                "dest": "IDLE",
-            },
-            {
-                "source": "RESETTING",
-                "trigger": "reset_failed",
-                "dest": "FAULT",
-            },
-            {
-                "source": ["ABORTED", "FAULT"],
-                "trigger": "restart_started",
-                "dest": "RESTARTING",
-            },
-            {
-                "source": "RESTARTING",
-                "trigger": "restart_succeeded",
-                "dest": "EMPTY",
-            },
-            {
-                "source": "RESTARTING",
-                "trigger": "restart_failed",
-                "dest": "FAULT",
-            },
-        ]
-
-        super().__init__(
-            states=states,
-            initial="EMPTY",
-            transitions=transitions,
-            after_state_change=self._state_changed,
-            **extra_kwargs,
-        )
-        self._state_changed()
-
-    def _state_changed(self):
-        """
-        State machine callback that is called every time the obs_state
-        changes. Responsible for ensuring that callbacks are called.
-        """
-        if self._callback is not None:
-            self._callback(self.state)
diff --git a/src/ska_tango_base/subarray/__init__.py b/src/ska_tango_base/subarray/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..42bdc884b54116a878f7ad769435722d4d835b32
--- /dev/null
+++ b/src/ska_tango_base/subarray/__init__.py
@@ -0,0 +1,20 @@
+"""
+This subpackage models a SKA subarray Tango device.
+"""
+
+__all__ = (
+    "SubarrayObsStateModel",
+    "SubarrayComponentManager",
+    "ReferenceSubarrayComponentManager",
+    "check_on",
+    "SKASubarray",
+)
+
+# Note: order of imports is important - start with lowest in the hierarchy
+from .subarray_obs_state_model import SubarrayObsStateModel
+
+from .component_manager import SubarrayComponentManager
+from .reference_component_manager import (
+    ReferenceSubarrayComponentManager, check_on
+)
+from .subarray_device import SKASubarray
diff --git a/src/ska_tango_base/subarray/component_manager.py b/src/ska_tango_base/subarray/component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a65500d84b3a09591bb177aaa314b5d9b151aa3
--- /dev/null
+++ b/src/ska_tango_base/subarray/component_manager.py
@@ -0,0 +1,165 @@
+"""
+This module provides an abstract component manager for SKA Tango
+subarray devices.
+"""
+from ska_tango_base.base import BaseComponentManager
+
+
+class SubarrayComponentManager(BaseComponentManager):
+    """
+    An abstract base class for a component manager for an SKA subarray
+    Tango devices, supporting:
+
+    * Maintaining a connection to its component
+
+    * Controlling its component via commands like AssignResources(),
+      Configure(), Scan(), etc.
+
+    * Monitoring its component, e.g. detect that a scan has completed
+    """
+
+    def __init__(self, op_state_model, obs_state_model):
+        """
+        Initialise a new SubarrayComponentManager instance
+
+        :param op_state_model: the op state model used by this component manager
+        :param obs_state_model: the obs state model used by this component manager
+        """
+        self.obs_state_model = obs_state_model
+
+        super().__init__(op_state_model)
+
+    def assign(self, resources):
+        """
+        Assign resources to the component
+
+        :param resources: resources to be assigned
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def release(self, resources):
+        """
+        Release resources from the component
+
+        :param resources: resources to be released
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def release_all(self):
+        """
+        Release all resources
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def configure(self, configuration):
+        """
+        Configure the component
+
+        :param configuration: the configuration to be configured
+        :type configuration: dict
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def deconfigure(self):
+        """
+        Deconfigure this component.
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def scan(self, args):
+        """
+        Start scanning
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def end_scan(self):
+        """
+        End scanning
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def abort(self):
+        """
+        Tell the component to abort whatever it was doing
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def obsreset(self):
+        """
+        Tell the component to reset to an unconfigured state (but
+        without releasing any assigned resources)
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def restart(self):
+        """
+        Tell the component to return to an empty state (unconfigured and
+        without any assigned resources)
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    @property
+    def assigned_resources(self):
+        """
+        Resources assigned to the component
+
+        :return: the resources assigned to the component
+        :rtype: list of str
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    @property
+    def configured_capabilities(self):
+        """
+        Configured capabilities of the component
+
+        :return: list of strings indicating number of configured
+            instances of each capability type
+        :rtype: list of str
+        """
+        raise NotImplementedError("SubarrayComponentManager is abstract.")
+
+    def component_resourced(self, resourced):
+        """
+        Callback hook, called when whether the component has any
+        resources changes
+
+        :param resourced: whether this component has any resources
+        :type resourced: bool
+        """
+        if resourced:
+            self.obs_state_model.perform_action("component_resourced")
+        else:
+            self.obs_state_model.perform_action("component_unresourced")
+
+    def component_configured(self, configured):
+        """
+        Callback hook, called when whether the component is configured
+        changes
+
+        :param configured: whether this component is configured
+        :type configured: bool
+        """
+        if configured:
+            self.obs_state_model.perform_action("component_configured")
+        else:
+            self.obs_state_model.perform_action("component_unconfigured")
+
+    def component_scanning(self, scanning):
+        """
+        Callback hook, called when whether the component is scanning
+        changes
+
+        :param scanning: whether this component is scanning
+        :type scanning: bool
+        """
+        if scanning:
+            self.obs_state_model.perform_action("component_scanning")
+        else:
+            self.obs_state_model.perform_action("component_not_scanning")
+
+    def component_obsfault(self):
+        """
+        Callback hook, called when the component obsfaults
+        """
+        self.obs_state_model.perform_action("component_obsfault")
diff --git a/src/ska_tango_base/subarray/reference_component_manager.py b/src/ska_tango_base/subarray/reference_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd22716e505eeb81540a19de3a0c8cced539306f
--- /dev/null
+++ b/src/ska_tango_base/subarray/reference_component_manager.py
@@ -0,0 +1,670 @@
+"""
+This module models component management for SKA subarray devices.
+"""
+import functools
+
+from tango import DevState
+
+from ska_tango_base.subarray import SubarrayComponentManager
+from ska_tango_base.base import (
+    check_communicating,
+    ReferenceBaseComponentManager,
+)
+from ska_tango_base.control_model import PowerMode
+from ska_tango_base.faults import (
+    CapabilityValidationError,
+    ComponentError,
+    ComponentFault,
+)
+
+
+def check_on(func):
+    """
+    Decorator that makes a method first checks that the component is
+    turned on and not faulty before allowing the command to proceed
+
+    :param func: the wrapped function
+
+    :return: the wrapped function
+    """
+    @functools.wraps(func)
+    def _wrapper(component, *args, **kwargs):
+        """
+        Wrapper function that checks that the component is turned on and
+        not faulty before invoking the wrapped function
+
+        :param component: the component to check
+        :param args: positional arguments to the wrapped function
+        :param kwargs: keyword arguments to the wrapped function
+
+        :return: whatever the wrapped function returns
+        """
+        if component.faulty:
+            raise ComponentFault()
+        if component.power_mode != PowerMode.ON:
+            raise ComponentError("Component is not ON")
+        return func(component, *args, **kwargs)
+
+    return _wrapper
+
+
+class ReferenceSubarrayComponentManager(
+    ReferenceBaseComponentManager, SubarrayComponentManager
+):
+    """
+    A component manager for SKA subarray Tango devices:
+
+    The current implementation is intended to
+    * illustrate the model
+    * enable testing of the base classes
+
+    It should not generally be used in concrete devices; instead, write
+    a subclass specific to the component managed by the device.
+    """
+
+    class _ResourcePool:
+        """
+        A simple class for managing subarray resources
+        """
+
+        def __init__(self, callback=None):
+            """
+            Initialise a new instance
+
+            :param callback: callback to call when the resource pool
+                goes from empty to non-empty or vice-versa
+            """
+            self._resources = set()
+
+            self._nonempty = False
+            self._callback = callback
+
+        def __len__(self):
+            """
+            Returns the number of resources currently assigned. Note that
+            this also functions as a boolean method for whether there are
+            any assigned resources: ``if len()``.
+
+            :return: number of resources assigned
+            :rtype: int
+            """
+            return len(self._resources)
+
+        def assign(self, resources):
+            """
+            Assign some resources
+
+            :param resources: resources to be assigned
+            :type resources: set(str)
+            """
+            self._resources |= set(resources)
+            self._update()
+
+        def release(self, resources):
+            """
+            Release some resources
+
+            :param resources: resources to be released
+            :type resources: set(str)
+            """
+            self._resources -= set(resources)
+            self._update()
+
+        def release_all(self):
+            """
+            Release all resources
+            """
+            self._resources.clear()
+            self._update()
+
+        def get(self):
+            """
+            Get current resources
+
+            :return: current resources.
+            :rtype: set(str)
+            """
+            return set(self._resources)
+
+        def check(self, resources):
+            """
+            Check that this pool contains specified resources.
+
+            This is useful for commands like configure(), which might
+            need to check that the subarray has the resources needed to
+            effect a configuration.
+
+            :return: whether this resource pool contains the specified
+                resources
+            :rtype bool
+            """
+            return resources in self._resources
+
+        def _update(self):
+            nonempty = bool(len(self))
+            if self._nonempty != nonempty:
+                self._nonempty = nonempty
+                if self._callback is not None:
+                    self._callback(nonempty)
+
+    class _Component(ReferenceBaseComponentManager._Component):
+        """
+        An example subarray component for the component manager to work
+        with.
+
+        It can be directly controlled via configure(), scan(),
+        end_scan(), end(), abort(), reset() and restart() command
+        methods.
+
+        For testing purposes, it can also be told to simulate an
+        observation fault via simulate_obsfault() methods.
+
+        When a component changes state, it lets the component manager
+        know by calling its ``component_unconfigured``,
+        ``component_configured``, ``component_scanning``,
+        ``component_not_scanning`` and ``component_obsfault`` methods.
+        """
+        def __init__(
+            self,
+            capability_types,
+            _power_mode=PowerMode.OFF,
+            _faulty=False,
+        ):
+            """
+            Initialise a new instance
+
+            :param capability_types: a list strings representing
+                capability types.
+            :param _power_mode: initial power mode of this component
+                (for testing only)
+            :param _faulty: whether this component should initially
+                simulate a fault (for testing only)
+            """
+            self._configured = False
+
+            # self._configured_capabilities is kept as a
+            # dictionary internally. The keys and values will represent
+            # the capability type name and the number of instances,
+            # respectively.
+            try:
+                self._configured_capabilities = dict.fromkeys(capability_types, 0)
+            except TypeError:
+                # Might need to have the device property be mandatory in the database.
+                self._configured_capabilities = {}
+
+            self._configured_callback = None
+
+            self._scanning = False
+            self._scanning_callback = None
+
+            self._obsfault = False
+            self._obsfault_callback = None
+
+            super().__init__(_power_mode=_power_mode, _faulty=_faulty)
+
+        def set_obs_callbacks(
+            self,
+            configured_callback,
+            scanning_callback,
+            obsfault_callback,
+        ):
+            """
+            Set callbacks for the underlying component
+
+            :param configured_callback: a callback to call with a
+                boolean argument when whether the component is
+                configured changes
+            :param scanning_callback: a callback to call with a boolean
+                argument when whether the component is scanning changes
+            :param obsfault_callback: a callback to call when the
+                component experiences an obs faults
+            """
+            self._configured_callback = configured_callback
+            self._scanning_callback = scanning_callback
+            self._obsfault_callback = obsfault_callback
+
+        @property
+        @check_on
+        def configured(self):
+            """
+            Whether this component is configured
+
+            :return: whether this component is configured
+            :rtype: bool
+            """
+            return self._configured
+
+        @property
+        @check_on
+        def configured_capabilities(self):
+            """
+            Configured capabilities of this component
+
+            :return: list of strings indicating number of configured
+                instances of each capability type
+            :rtype: list of str
+            """
+            configured_capabilities = []
+            for capability_type, capability_instances in list(
+                self._configured_capabilities.items()
+            ):
+                configured_capabilities.append(
+                    "{}:{}".format(capability_type, capability_instances)
+                )
+            return sorted(configured_capabilities)
+
+        @property
+        @check_on
+        def scanning(self):
+            """
+            Whether this component is scanning
+
+            :return: whether this component is scanning
+            :rtype: bool
+            """
+            return self._scanning
+
+        @property
+        @check_on
+        def obsfault(self):
+            """
+            Whether this component is obsfaulting
+
+            :return: whether this component is obsfaulting
+            :rtype: bool
+            """
+            return self._obsfault
+
+        def _validate_capability_types(self, capability_types):
+            """
+            Check the validity of the input parameter passed to the
+            Configure command.
+
+            :param capability_types: a list strings representing
+                capability types.
+            :type capability_types: list
+
+            :raises CapabilityValidationError: If any of the capabilities
+                requested are not valid.
+            """
+            invalid_capabilities = list(
+                set(capability_types) - set(self._configured_capabilities)
+            )
+
+            if invalid_capabilities:
+                raise CapabilityValidationError(
+                    "Invalid capability types requested {}".format(invalid_capabilities)
+                )
+
+        @check_on
+        def configure(self, configuration):
+            """
+            Configure the component
+
+            :param configuration: the configuration to be configured
+            :type configuration: dict
+            """
+            # In this example implementation, the keys of the dict
+            # are the capability types, and the values are the
+            # integer number of instances required.
+            # E.g., config = {"BAND1": 5, "BAND2": 3}
+            capability_types = list(configuration.keys())
+            self._validate_capability_types(capability_types)
+
+            # Perform the configuration.
+            for capability_type, capability_instances in configuration.items():
+                self._configured_capabilities[capability_type] += capability_instances
+
+            self._update_configured(True)
+
+        @check_on
+        def deconfigure(self):
+            """
+            Deconfigure this component.
+            """
+            self._configured_capabilities = {
+                k: 0 for k in self._configured_capabilities
+            }
+            self._update_configured(False)
+
+        @check_on
+        def scan(self, args):
+            """
+            Start scanning
+            """
+            self._update_scanning(True)
+
+        @check_on
+        def end_scan(self):
+            """
+            End scanning
+            """
+            self.simulate_scan_stopped()
+
+        @check_on
+        def simulate_scan_stopped(self):
+            """
+            Tell the component to simulate spontaneous stopping its
+            scan.
+            """
+            self._update_scanning(False)
+
+        @check_on
+        def simulate_obsfault(self, obsfault):
+            """
+            Tell the component to simulate an obsfault
+            """
+            self._update_obsfault(obsfault)
+
+        def _invoke_configured_callback(self):
+            """
+            Helper method that invokes the callback when whether the
+            component is configured changes.
+            """
+            if not self.faulty:
+                if self._configured_callback is not None:
+                    self._configured_callback(self._configured)
+
+        def _update_configured(self, configured):
+            """
+            Helper method that updates whether the component is
+            configured or not, ensuring that callbacks are called as
+            required.
+
+            :param configured: new value for whether the component is
+                configured or not
+            :type configured: bool
+            """
+            if self._configured != configured:
+                self._configured = configured
+                self._invoke_configured_callback()
+
+        def _invoke_scanning_callback(self):
+            """
+            Helper method that invokes the callback when whether the
+            component is scanning changes.
+            """
+            if not self.faulty:
+                if self._scanning_callback is not None:
+                    self._scanning_callback(self._scanning)
+
+        def _update_scanning(self, scanning):
+            """
+            Helper method that updates whether the component is
+            scanning or not, ensuring that callbacks are called as
+            required.
+
+            :param scanning: new value for whether the component is
+                scanning or not
+            :type scanning: bool
+            """
+            if self._scanning != scanning:
+                self._scanning = scanning
+                self._invoke_scanning_callback()
+
+        def _invoke_obsfault_callback(self):
+            """
+            Helper method that invokes the callback when the component
+            experiences an obsfault.
+            """
+            if not self.faulty:
+                if self.obsfault and self._obsfault_callback is not None:
+                    self._obsfault_callback()
+
+        def _update_obsfault(self, obsfault):
+            """
+            Helper method that updates whether the component is
+            obsfaulting or not, ensuring that callbacks are called as
+            required.
+
+            :param obsfault: new value for whether the component is
+                obsfaulting or not
+            :type obsfaulting: bool
+            """
+            if self._obsfault != obsfault:
+                self._obsfault = obsfault
+                if obsfault:
+                    self._invoke_obsfault_callback()
+
+    def __init__(
+        self, op_state_model, obs_state_model, capability_types, logger=None, _component=None
+    ):
+        """
+        Initialise a new ReferenceSubarrayComponentManager instance
+
+        :param op_state_model: the op state model used by this component
+            manager
+        :param obs_state_model: the obs state model used by this
+            component manager
+        :param capability_types: types of capability supported by this
+            component manager
+        :param logger: a logger for this component manager
+        :param _component: allows setting of the component to be
+            managed; for testing purposes only
+        """
+        self.obs_state_model = obs_state_model
+        self._resource_pool = self._ResourcePool(self.component_resourced)
+
+        super().__init__(
+            op_state_model,
+            obs_state_model,
+            logger=logger,
+            _component=_component or self._Component(capability_types),
+        )
+
+    def start_communicating(self):
+        """
+        Establish communication with the component, then start
+        monitoring.
+        """
+        if self._connected:
+            return
+        super().start_communicating()
+
+        self._component.set_obs_callbacks(
+            self.component_configured,
+            self.component_scanning,
+            self.component_obsfault,
+        )
+
+        if self._component.faulty:
+            return
+        if self._component.power_mode != PowerMode.ON:
+            return
+
+        # we've been disconnected and we might have missed some
+        # changes, so we need to check the component's state, and
+        # make our state model correspond
+        if self._component.obsfault:
+            self.obs_state_model.to_OBSFAULT()
+        elif not self._component.configured:
+            self.obs_state_model.to_IDLE()
+        elif not self._component.scanning:
+            self.obs_state_model.to_READY()
+        else:
+            self.obs_state_model.to_SCANNING()
+
+    def stop_communicating(self):
+        """
+        Cease monitoring the component, and break off all communication
+        with it.
+        """
+        if not self._connected:
+            return
+
+        self._component.set_obs_callbacks(None, None, None)
+        super().stop_communicating()
+
+    def simulate_communication_failure(self, fail_communicate):
+        """
+        Simulate (or stop simulating) a component connection failure
+
+        :param fail_communicate: whether the connection to the component
+            is failing
+        """
+        if fail_communicate and self._connected:
+            self._component.set_obs_callbacks(None, None, None)
+        super().simulate_communication_failure(fail_communicate)
+
+    @check_communicating
+    def assign(self, resources):
+        """
+        Assign resources to the component
+
+        :param resources: resources to be assigned
+        :type resources: list(str)
+        """
+        self.logger.info("Assigning resources to component")
+        self._resource_pool.assign(resources)
+
+    @check_communicating
+    def release(self, resources):
+        """
+        Release resources from the component
+
+        :param resources: resources to be released
+        :type resources: list(str)
+        """
+        self.logger.info("Releasing resources in component")
+        self._resource_pool.release(resources)
+
+    @check_communicating
+    def release_all(self):
+        """
+        Release all resources
+        """
+        self.logger.info("Releasing all resources in component")
+        self._resource_pool.release_all()
+
+    @check_communicating
+    def configure(self, configuration):
+        """
+        Configure the component
+
+        :param configuration: the configuration to be configured
+        :type configuration: dict
+        """
+        self.logger.info("Configuring component")
+        self._component.configure(configuration)
+
+    @check_communicating
+    def deconfigure(self):
+        """
+        Deconfigure this component.
+        """
+        self.logger.info("Deconfiguring component")
+        self._component.deconfigure()
+
+    @check_communicating
+    def scan(self, args):
+        """
+        Start scanning
+        """
+        self.logger.info("Starting scan in component")
+        self._component.scan(args)
+
+    @check_communicating
+    def end_scan(self):
+        """
+        End scanning
+        """
+        self.logger.info("Stopping scan in component")
+        self._component.end_scan()
+
+    @check_communicating
+    def abort(self):
+        """
+        Tell the component to abort whatever it was doing
+        """
+        self.logger.info("Aborting component")
+        if self._component.scanning:
+            self._component.end_scan()
+
+    @check_communicating
+    def obsreset(self):
+        """
+        Tell the component to reset to an unconfigured state (but
+        without releasing any assigned resources)
+        """
+        self.logger.info("Resetting component")
+        if self._component.configured:
+            self._component.deconfigure()
+
+    @check_communicating
+    def restart(self):
+        """
+        Tell the component to return to an empty state (unconfigured and
+        without any assigned resources)
+        """
+        self.logger.info("Restarting component")
+        if self._component.configured:
+            self._component.deconfigure()
+        self._resource_pool.release_all()
+
+    @property
+    @check_communicating
+    def assigned_resources(self):
+        """
+        Resources assigned to the component
+
+        :return: the resources assigned to the component
+        :rtype: list of str
+        """
+        return sorted(self._resource_pool.get())
+
+    @property
+    @check_communicating
+    def configured_capabilities(self):
+        """
+        Configured capabilities of the component
+
+        :return: list of strings indicating number of configured
+            instances of each capability type
+        :rtype: list of str
+        """
+        return self._component.configured_capabilities
+
+    def component_resourced(self, resourced):
+        """
+        Callback hook, called when whether the component has any
+        resources changes
+
+        :param resourced: whether this component has any resources
+        :type resourced: bool
+        """
+        if resourced:
+            self.obs_state_model.perform_action("component_resourced")
+        else:
+            self.obs_state_model.perform_action("component_unresourced")
+
+    def component_configured(self, configured):
+        """
+        Callback hook, called when whether the component is configured
+        changes
+
+        :param configured: whether this component is configured
+        :type configured: bool
+        """
+        if configured:
+            self.obs_state_model.perform_action("component_configured")
+        else:
+            self.obs_state_model.perform_action("component_unconfigured")
+
+    def component_scanning(self, scanning):
+        """
+        Callback hook, called when whether the component is scanning
+        changes
+
+        :param scanning: whether this component is scanning
+        :type scanning: bool
+        """
+        if scanning:
+            self.obs_state_model.perform_action("component_scanning")
+        else:
+            self.obs_state_model.perform_action("component_not_scanning")
+
+    def component_obsfault(self):
+        """
+        Callback hook, called when the component obsfaults
+        """
+        self.obs_state_model.perform_action("component_obsfault")
diff --git a/src/ska_tango_base/subarray_device.py b/src/ska_tango_base/subarray/subarray_device.py
similarity index 56%
rename from src/ska_tango_base/subarray_device.py
rename to src/ska_tango_base/subarray/subarray_device.py
index 044bdd1d4022fcb0ddc02f46eb7ca3c24ef868ac..170c89587f935e3cc58a81feeaa4b212cd6a5733 100644
--- a/src/ska_tango_base/subarray_device.py
+++ b/src/ska_tango_base/subarray/subarray_device.py
@@ -14,172 +14,18 @@ information like assigned resources, configured capabilities, etc.
 import json
 import warnings
 
-from tango import DebugIt, DevState
+from tango import DebugIt
 from tango.server import run, attribute, command
 from tango.server import device_property
 
 # SKA specific imports
-from ska_tango_base import SKAObsDevice, ObsDeviceStateModel
-from ska_tango_base.commands import ActionCommand, ResultCode
-from ska_tango_base.control_model import AdminMode, ObsState
-from ska_tango_base.faults import CapabilityValidationError, StateModelError
-from ska_tango_base.state_machine import ObservationStateMachine
-from ska_tango_base.utils import for_testing_only
+from ska_tango_base import SKAObsDevice
+from ska_tango_base.commands import CompletionCommand, ObservationCommand, ResponseCommand, ResultCode
+from ska_tango_base.subarray import SubarrayComponentManager, SubarrayObsStateModel
 
 # PROTECTED REGION END #    //  SKASubarray.additionnal_imports
 
-__all__ = ["SKASubarray", "SKASubarrayStateModel", "main"]
-
-
-class SKASubarrayStateModel(ObsDeviceStateModel):
-    """
-    Implements the state model for the SKASubarray
-    """
-
-    def __init__(
-        self,
-        logger,
-        op_state_callback=None,
-        admin_mode_callback=None,
-        obs_state_callback=None,
-    ):
-        """
-        Initialises the model. Note that this does not imply moving to
-        INIT state. The INIT state is managed by the model itself.
-
-        :param logger: the logger to be used by this state model.
-        :type logger: a logger that implements the standard library
-            logger interface
-        :param op_state_callback: A callback to be called when a
-            transition implies a change to op state
-        :type op_state_callback: callable
-        :param admin_mode_callback: A callback to be called when a
-            transition causes a change to device admin_mode
-        :type admin_mode_callback: callable
-        :param obs_state_callback: A callback to be called when a
-            transition causes a change to device obs_state
-        :type obs_state_callback: callable
-        """
-        action_breakdown = {
-            # "action": ("action_on_obs_machine", "action_on_superclass"),
-            "off_succeeded": ("to_EMPTY", "off_succeeded"),
-            "off_failed": ("to_EMPTY", "off_failed"),
-            "on_succeeded": (None, "on_succeeded"),
-            "on_failed": ("to_EMPTY", "on_failed"),
-            "assign_started": ("assign_started", None),
-            "release_started": ("release_started", None),
-            "resourcing_succeeded_some_resources": (
-                "resourcing_succeeded_some_resources",
-                None,
-            ),
-            "resourcing_succeeded_no_resources": (
-                "resourcing_succeeded_no_resources",
-                None,
-            ),
-            "resourcing_failed": ("resourcing_failed", None),
-            "configure_started": ("configure_started", None),
-            "configure_succeeded": ("configure_succeeded", None),
-            "configure_failed": ("configure_failed", None),
-            "scan_started": ("scan_started", None),
-            "scan_succeeded": ("scan_succeeded", None),
-            "scan_failed": ("scan_failed", None),
-            "end_scan_succeeded": ("end_scan_succeeded", None),
-            "end_scan_failed": ("end_scan_failed", None),
-            "end_succeeded": ("end_succeeded", None),
-            "end_failed": ("end_failed", None),
-            "abort_started": ("abort_started", None),
-            "abort_succeeded": ("abort_succeeded", None),
-            "abort_failed": ("abort_failed", None),
-            "obs_reset_started": ("reset_started", None),
-            "obs_reset_succeeded": ("reset_succeeded", None),
-            "obs_reset_failed": ("reset_failed", None),
-            "restart_started": ("restart_started", None),
-            "restart_succeeded": ("restart_succeeded", None),
-            "restart_failed": ("restart_failed", None),
-            "fatal_error": ("fatal_error", None),
-        }
-
-        super().__init__(
-            action_breakdown,
-            ObservationStateMachine,
-            logger,
-            op_state_callback=op_state_callback,
-            admin_mode_callback=admin_mode_callback,
-            obs_state_callback=obs_state_callback,
-        )
-
-
-class SKASubarrayResourceManager:
-    """
-    A simple class for managing subarray resources
-    """
-
-    def __init__(self, key: str = "example"):
-        """
-        Constructor for SKASubarrayResourceManager
-
-        :param key: Key used to select from JSON input to assign/release methods.
-        """
-        self._resources = set()
-        self._key = key
-
-    def __len__(self):
-        """
-        Returns the number of resources currently assigned. Note that
-        this also functions as a boolean method for whether there are
-        any assigned resources: ``if len()``.
-
-        :return: number of resources assigned
-        :rtype: int
-        """
-        return len(self._resources)
-
-    def assign(self, resources):
-        """
-        Assign some resources
-
-        :todo: Currently implemented for testing purposes to take a JSON
-            string encoding a dictionary. In future this
-            will take a collection of resources.
-
-        :param resources: JSON-encoding of a dictionary, with resources to
-            assign under the configured key (default 'example')
-        :type resources: JSON string
-        """
-        resources_dict = json.loads(resources)
-        add_resources = resources_dict[self._key]
-        self._resources |= set(add_resources)
-
-    def release(self, resources):
-        """
-        Release some resources
-
-        :todo: Currently implemented for testing purposes to take a JSON
-            string encoding a dictionary. In future this
-            will take a collection of resources.
-
-        :param resources: JSON-encoding of a dictionary, with resources to
-            assign under the configured key (default 'example')
-        :type resources: JSON string
-        """
-        resources_dict = json.loads(resources)
-        drop_resources = resources_dict[self._key]
-        self._resources -= set(drop_resources)
-
-    def release_all(self):
-        """
-        Release all resources
-        """
-        self._resources.clear()
-
-    def get(self):
-        """
-        Get current resources
-
-        :return: a set of current resources.
-        :rtype: set of string
-        """
-        return set(self._resources)
+__all__ = ["SKASubarray", "main"]
 
 
 class SKASubarray(SKAObsDevice):
@@ -204,98 +50,38 @@ class SKASubarray(SKAObsDevice):
             super().do()
 
             device = self.target
-            device.resource_manager = SKASubarrayResourceManager()
             device._activation_time = 0.0
 
-            # device._configured_capabilities is kept as a
-            # dictionary internally. The keys and values will represent
-            # the capability type name and the number of instances,
-            # respectively.
-            try:
-                device._configured_capabilities = dict.fromkeys(
-                    device.CapabilityTypes,
-                    0
-                )
-            except TypeError:
-                # Might need to have the device property be mandatory in the database.
-                device._configured_capabilities = {}
-
             message = "SKASubarray Init command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class _ResourcingCommand(ActionCommand):
-        """
-        An abstract base class for SKASubarray's resourcing commands.
-        """
-
-        def __init__(self, target, state_model, action_hook, logger=None):
-            """
-            Constructor for _ResourcingCommand
 
-            :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
-            :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
-            :param action_hook: a hook for the command, used to build
-                actions that will be sent to the state model; for example,
-                if the hook is "scan", then success of the command will
-                result in action "scan_succeeded" being sent to the state
-                model.
-            :type action_hook: string
-            :param logger: the logger to be used by this Command. If not
-                provided, then a default module logger will be used.
-            :type logger: a logger that implements the standard library
-                logger interface
-            """
-            super().__init__(
-                target, state_model, action_hook, start_action=True, logger=logger
-            )
-
-        def succeeded(self):
-            """
-            Action to take on successful completion of a resourcing
-            command.
-            """
-            if len(self.target):
-                action = "resourcing_succeeded_some_resources"
-            else:
-                action = "resourcing_succeeded_no_resources"
-            self.state_model.perform_action(action)
-
-        def failed(self):
-            """
-            Action to take on failed completion of a resourcing command.
-            """
-            self.state_model.perform_action("resourcing_failed")
-
-    class AssignResourcesCommand(_ResourcingCommand):
+    class AssignResourcesCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's AssignResources() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for AssignResourcesCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "assign", logger=logger)
+            super().__init__(target, obs_state_model, "assign", op_state_model, logger=logger)
 
         def do(self, argin):
             """
@@ -309,36 +95,38 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            resource_manager = self.target
-            resource_manager.assign(argin)
+            component_manager = self.target
+            component_manager.assign(argin)
 
             message = "AssignResources command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class ReleaseResourcesCommand(_ResourcingCommand):
+    class ReleaseResourcesCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's ReleaseResources() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
-            Constructor for OnCommand()
+            Constructor for ReleaseResourcesCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "release", logger=logger)
+            super().__init__(target, obs_state_model, "release", op_state_model, logger=logger)
 
         def do(self, argin):
             """
@@ -352,17 +140,37 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            resource_manager = self.target
-            resource_manager.release(argin)
+            component_manager = self.target
+            component_manager.release(argin)
 
             message = "ReleaseResources command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class ReleaseAllResourcesCommand(ReleaseResourcesCommand):
+    class ReleaseAllResourcesCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's ReleaseAllResources() command.
         """
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
+            """
+            Constructor for ReleaseResourcesCommand
+
+            :param target: the object that this command acts upon; for
+                example, the device's component manager
+            :type target: object
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
+            :param logger: the logger to be used by this Command. If not
+                provided, then a default module logger will be used.
+            :type logger: a logger that implements the standard library
+                logger interface
+            """
+            super().__init__(target, obs_state_model, "release", op_state_model, logger=logger)
 
         def do(self):
             """
@@ -373,42 +181,39 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            resource_manager = self.target
-            resource_manager.release_all()
+            component_manager = self.target
+            component_manager.release_all()
 
-            if len(resource_manager):
-                message = "ReleaseAllResources command failed to release all."
-                self.logger.info(message)
-                return (ResultCode.FAILED, message)
-            else:
-                message = "ReleaseAllResources command completed OK"
-                self.logger.info(message)
-                return (ResultCode.OK, message)
+            message = "ReleaseAllResources command completed OK"
+            self.logger.info(message)
+            return (ResultCode.OK, message)
 
-    class ConfigureCommand(ActionCommand):
+    class ConfigureCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's Configure() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ConfigureCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "configure", start_action=True, logger=logger
+                target, obs_state_model, "configure", op_state_model, logger=logger
             )
 
         def do(self, argin):
@@ -423,48 +228,39 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-
-            # In this example implementation, the keys of the dict
-            # are the capability types, and the values are the
-            # integer number of instances required.
-            # E.g., config = {"BAND1": 5, "BAND2": 3}
-            config = json.loads(argin)
-            capability_types = list(config.keys())
-            device._validate_capability_types(capability_types)
-
-            # Perform the configuration.
-            for capability_type, capability_instances in config.items():
-                device._configured_capabilities[capability_type] += capability_instances
+            component_manager = self.target
+            component_manager.configure(argin)
 
             message = "Configure command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class ScanCommand(ActionCommand):
+    class ScanCommand(ObservationCommand, ResponseCommand):
         """
         A class for SKASubarray's Scan() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ScanCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "scan", start_action=True, logger=logger
+                target, obs_state_model, "scan", op_state_model, logger=logger
             )
 
         def do(self, argin):
@@ -479,34 +275,40 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            # we do a json.loads just for basic string validation
-            message = f"Scan command STARTED - config {json.loads(argin)}"
+            component_manager = self.target
+            component_manager.scan(argin)
+
+            message = f"Scan command started"
             self.logger.info(message)
             return (ResultCode.STARTED, message)
 
-    class EndScanCommand(ActionCommand):
+    class EndScanCommand(ObservationCommand, ResponseCommand):
         """
         A class for SKASubarray's EndScan() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for EndScanCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "end_scan", logger=logger)
+            super().__init__(
+                target, obs_state_model, "end_scan", op_state_model, logger=logger
+            )
 
         def do(self):
             """
@@ -517,33 +319,40 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            component_manager = self.target
+            component_manager.end_scan()
+
             message = "EndScan command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class EndCommand(ActionCommand):
+    class EndCommand(ObservationCommand, ResponseCommand):
         """
         A class for SKASubarray's End() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for EndCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
-            super().__init__(target, state_model, "end", logger=logger)
+            super().__init__(
+                target, obs_state_model, "end", op_state_model, logger=logger
+            )
 
         def do(self):
             """
@@ -554,37 +363,39 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-            device._deconfigure()
+            component_manager = self.target
+            component_manager.deconfigure()
 
             message = "End command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class AbortCommand(ActionCommand):
+    class AbortCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's Abort() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for AbortCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "abort", start_action=True, logger=logger
+                target, obs_state_model, "abort", op_state_model, logger=logger
             )
 
         def do(self):
@@ -596,34 +407,39 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
+            component_manager = self.target
+            component_manager.abort()
+
             message = "Abort command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class ObsResetCommand(ActionCommand):
+    class ObsResetCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's ObsReset() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for ObsResetCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "obs_reset", start_action=True, logger=logger
+                target, obs_state_model, "obsreset", op_state_model, logger=logger
             )
 
         def do(self):
@@ -635,42 +451,39 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-
-            # We might have interrupted a long-running command such as a Configure
-            # or a Scan, so we need to clean up from that.
-
-            # Now totally deconfigure
-            device._deconfigure()
+            component_manager = self.target
+            component_manager.obsreset()
 
             message = "ObsReset command completed OK"
             self.logger.info(message)
             return (ResultCode.OK, message)
 
-    class RestartCommand(ActionCommand):
+    class RestartCommand(ObservationCommand, ResponseCommand, CompletionCommand):
         """
         A class for SKASubarray's Restart() command.
         """
 
-        def __init__(self, target, state_model, logger=None):
+        def __init__(self, target, op_state_model, obs_state_model, logger=None):
             """
             Constructor for RestartCommand
 
             :param target: the object that this command acts upon; for
-                example, the SKASubarray device for which this class
-                implements the command
+                example, the device's component manager
             :type target: object
-            :param state_model: the state model that this command uses
-                 to check that it is allowed to run, and that it drives
-                 with actions.
-            :type state_model: :py:class:`SKASubarrayStateModel`
+            :param op_state_model: the op state model that this command
+                uses to check that it is allowed to run
+            :type op_state_model: :py:class:`OpStateModel`
+            :param obs_state_model: the observation state model that
+                 this command uses to check that it is allowed to run,
+                 and that it drives with actions.
+            :type obs_state_model: :py:class:`SubarrayObsStateModel`
             :param logger: the logger to be used by this Command. If not
                 provided, then a default module logger will be used.
             :type logger: a logger that implements the standard library
                 logger interface
             """
             super().__init__(
-                target, state_model, "restart", start_action=True, logger=logger
+                target, obs_state_model, "restart", op_state_model, logger=logger
             )
 
         def do(self):
@@ -682,16 +495,8 @@ class SKASubarray(SKAObsDevice):
                 information purpose only.
             :rtype: (ResultCode, str)
             """
-            device = self.target
-
-            # We might have interrupted a long-running command such as a Configure
-            # or a Scan, so we need to clean up from that.
-
-            # Now totally deconfigure
-            device._deconfigure()
-
-            # and release all resources
-            device.resource_manager.release_all()
+            component_manager = self.target
+            component_manager.restart()
 
             message = "Restart command completed OK"
             self.logger.info(message)
@@ -702,70 +507,40 @@ class SKASubarray(SKAObsDevice):
         """
         Sets up the state model for the device
         """
-        self.state_model = SKASubarrayStateModel(
+        super()._init_state_model()
+        self.obs_state_model = SubarrayObsStateModel(
             logger=self.logger,
-            op_state_callback=self._update_state,
-            admin_mode_callback=self._update_admin_mode,
-            obs_state_callback=self._update_obs_state,
+            callback=self._update_obs_state
         )
 
+    def create_component_manager(self):
+        return SubarrayComponentManager(self.op_state_model, self.obs_state_model)
+
     def init_command_objects(self):
         """
         Sets up the command objects
         """
         super().init_command_objects()
 
-        device_args = (self, self.state_model, self.logger)
-        resource_args = (self.resource_manager, self.state_model, self.logger)
-
-        self.register_command_object(
-            "AssignResources", self.AssignResourcesCommand(*resource_args)
-        )
-        self.register_command_object(
-            "ReleaseResources", self.ReleaseResourcesCommand(*resource_args)
-        )
-        self.register_command_object(
-            "ReleaseAllResources", self.ReleaseAllResourcesCommand(*resource_args)
-        )
-        self.register_command_object("Configure", self.ConfigureCommand(*device_args))
-        self.register_command_object("Scan", self.ScanCommand(*device_args))
-        self.register_command_object("EndScan", self.EndScanCommand(*device_args))
-        self.register_command_object("End", self.EndCommand(*device_args))
-        self.register_command_object("Abort", self.AbortCommand(*device_args))
-        self.register_command_object("ObsReset", self.ObsResetCommand(*device_args))
-        self.register_command_object("Restart", self.RestartCommand(*device_args))
-
-    def _validate_capability_types(self, capability_types):
-        """
-        Check the validity of the input parameter passed to the
-        Configure command.
-
-        :param device: the device for which this class implements
-            the configure command
-        :type device: :py:class:`SKASubarray`
-        :param capability_types: a list strings representing
-            capability types.
-        :type capability_types: list
-
-        :raises CapabilityValidationError: If any of the capabilities
-            requested are not valid.
-        """
-        invalid_capabilities = list(
-            set(capability_types) - set(self._configured_capabilities))
-
-        if invalid_capabilities:
-            raise CapabilityValidationError(
-                "Invalid capability types requested {}".format(
-                    invalid_capabilities
+        for (command_name, command_class) in [
+            ("AssignResources", self.AssignResourcesCommand),
+            ("ReleaseResources", self.ReleaseResourcesCommand),
+            ("ReleaseAllResources", self.ReleaseAllResourcesCommand),
+            ("Configure", self.ConfigureCommand),
+            ("Scan", self.ScanCommand),
+            ("EndScan", self.EndScanCommand),
+            ("End", self.EndCommand),
+            ("Abort", self.AbortCommand),
+            ("ObsReset", self.ObsResetCommand),
+            ("Restart", self.RestartCommand),
+        ]:
+            self.register_command_object(
+                command_name,
+                command_class(
+                    self.component_manager, self.op_state_model, self.obs_state_model, self.logger
                 )
             )
 
-    def _deconfigure(self):
-        """
-        Completely deconfigure the subarray
-        """
-        self._configured_capabilities = {k: 0 for k in self._configured_capabilities}
-
     # -----------------
     # Device Properties
     # -----------------
@@ -845,7 +620,7 @@ class SKASubarray(SKAObsDevice):
 
         :return: Resources assigned to the device.
         """
-        return sorted(self.resource_manager.get())
+        return self.component_manager.assigned_resources  #pylint: disable=no-member
         # PROTECTED REGION END #    //  SKASubarray.assignedResources_read
 
     def read_configuredCapabilities(self):
@@ -856,13 +631,7 @@ class SKASubarray(SKAObsDevice):
         :return: A list of capability types with no. of instances used
             in the Subarray
         """
-        configured_capabilities = []
-        for capability_type, capability_instances in list(
-            self._configured_capabilities.items()
-        ):
-            configured_capabilities.append(
-                "{}:{}".format(capability_type, capability_instances))
-        return sorted(configured_capabilities)
+        return self.component_manager.configured_capabilities  #pylint: disable=no-member
         # PROTECTED REGION END #    //  SKASubarray.configuredCapabilities_read
 
     # --------
@@ -874,13 +643,11 @@ class SKASubarray(SKAObsDevice):
         Check if command `AssignResources` is allowed in the current
         device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("AssignResources")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_in="DevString",
@@ -905,21 +672,20 @@ class SKASubarray(SKAObsDevice):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("AssignResources")
-        (return_code, message) = command(argin)
+        args = json.loads(argin)
+        (return_code, message) = command(args)
         return [[return_code], [message]]
 
     def is_ReleaseResources_allowed(self):
         """
         Check if command `ReleaseResources` is allowed in the current
-        device state.
-
-        :raises ``tango.DevFailed``: if the command is not allowed
+        device state
 
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("ReleaseResources")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_in="DevString",
@@ -944,7 +710,8 @@ class SKASubarray(SKAObsDevice):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("ReleaseResources")
-        (return_code, message) = command(argin)
+        args = json.loads(argin)
+        (return_code, message) = command(args)
         return [[return_code], [message]]
 
     def is_ReleaseAllResources_allowed(self):
@@ -952,13 +719,11 @@ class SKASubarray(SKAObsDevice):
         Check if command `ReleaseAllResources` is allowed in the current
         device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("ReleaseAllResources")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -986,13 +751,11 @@ class SKASubarray(SKAObsDevice):
         Check if command `Configure` is allowed in the current
         device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Configure")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_in="DevString",
@@ -1017,20 +780,19 @@ class SKASubarray(SKAObsDevice):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("Configure")
-        (return_code, message) = command(argin)
+        args = json.loads(argin)
+        (return_code, message) = command(args)
         return [[return_code], [message]]
 
     def is_Scan_allowed(self):
         """
         Check if command `Scan` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Scan")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_in="DevString",
@@ -1055,20 +817,19 @@ class SKASubarray(SKAObsDevice):
         :rtype: (ResultCode, str)
         """
         command = self.get_command_object("Scan")
-        (return_code, message) = command(argin)
+        args = json.loads(argin)
+        (return_code, message) = command(args)
         return [[return_code], [message]]
 
     def is_EndScan_allowed(self):
         """
         Check if command `EndScan` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("EndScan")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1095,13 +856,11 @@ class SKASubarray(SKAObsDevice):
         """
         Check if command `End` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("End")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1129,13 +888,11 @@ class SKASubarray(SKAObsDevice):
         """
         Check if command `Abort` is allowed in the current device state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Abort")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1164,13 +921,11 @@ class SKASubarray(SKAObsDevice):
         Check if command `ObsReset` is allowed in the current device
         state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("ObsReset")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
@@ -1198,13 +953,11 @@ class SKASubarray(SKAObsDevice):
         Check if command `Restart` is allowed in the current device
         state.
 
-        :raises ``tango.DevFailed``: if the command is not allowed
-
         :return: ``True`` if the command is allowed
         :rtype: boolean
         """
         command = self.get_command_object("Restart")
-        return command.check_allowed()
+        return command.is_allowed(raise_if_disallowed=True)
 
     @command(
         dtype_out='DevVarLongStringArray',
diff --git a/src/ska_tango_base/subarray/subarray_obs_state_model.py b/src/ska_tango_base/subarray/subarray_obs_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c00bb9c07b242e53c893c4c00daea86caabf2a4
--- /dev/null
+++ b/src/ska_tango_base/subarray/subarray_obs_state_model.py
@@ -0,0 +1,437 @@
+"""
+This module specifies the observation state ("obs state") model for SKA
+subarray Tango devices. It consists of:
+
+* an underlying state machine: :py:class:`._SubarrayObsStateMachine`
+
+* an :py:class:`.SubarrayObsStateModel` that maps the underlying state
+  machine state to a value of the
+  :py:class:`ska_tango_base.control_model.ObsState` enum.
+  
+"""
+from transitions.extensions import LockedMachine as Machine
+
+from ska_tango_base.control_model import ObsState
+from ska_tango_base.obs import ObsStateModel
+
+__all__ = ["SubarrayObsStateModel"]
+
+
+class _SubarrayObsStateMachine(Machine):
+    """
+    State machine representing the observation state machine for
+    subarrays.
+    
+    The machine implemented is essentially as agreed in ADR-8, but with
+    some states broken down into sub-states to account for the
+    interactions between commands and monitoring of the underlying
+    component.
+    
+    For example, ADR-8 says that a configuring subarray moves from IDLE
+    to CONFIGURING to READY. But in a device model where the state
+    machine is responsive to both commands and changes to the monitored
+    component, the sequence is better represented as follows:
+
+    1. The Configure() command triggers the "configure_invoked" action
+       on the state machine, resulting in a transition from IDLE to
+       CONFIGURING_IDLE
+    2. The Configure() command invokes methods on its component in order
+       to effect configuration. At some point in this process, the
+       component triggers the "component_configured" action on the
+       state machine, resulting in a transition from CONFIGURING_IDLE to
+       CONFIGURING_READY.
+    3. At completion of configuration, the action "configure_completed"
+       is triggered on the state machine, resulting in a transition from
+       CONFIGURING_READY to READY.
+
+    Thus, this machine contains substates CONFIGURING_IDLE and
+    CONFIGURING_READY, rather than the ADR-8 state CONFIGURING
+
+    The full list of supported states are:
+
+    * **EMPTY**: the subarray is unresourced
+    * **RESOURCING_EMPTY**: the subarray is unresourced, but performing
+      a resourcing operation
+    * **RESOURCING_IDLE**: the subarray is resourced, and currently
+      performing a resourcing operation
+    * **IDLE**: the subarray is resourced but unconfigured
+    * **CONFIGURING_IDLE**: the subarray is resourced but unconfigured;
+      it is currently performing a configuring operation
+    * **CONFIGURING_READY**: the subarray is resourced and configured;
+      it is currently performing a configuring operation
+    * **READY**: the subarray is resourced and configured
+    * **SCANNING**: the subarray is scanning
+    * **ABORTING**: the subarray is aborting
+    * **ABORTED**: the subarray has aborted
+    * **RESETTING**: the subarray is resetting from an ABORTED or FAULT
+      state back to IDLE
+    * **RESTARTING**: the subarray is restarting from an ABORTED or
+      FAULT state back to EMPTY
+    * **FAULT**: the subarray has encountered a observation fault.
+
+    The actions supported divide into command-oriented actions and
+    component monitoring actions.
+
+    The command-oriented actions are:
+
+    * **assign_invoked** and **assign_completed**: bookending the
+      AssignResources() command, and hence the RESOURCING transitional
+      state
+    * **release_invoked** and **release_completed**: bookending the
+      ReleaseResources() and ReleaseAllResources() commands, hence the
+      RESOURCING transitional state
+    * **configure_invoked** and **configure_completed**: bookending the
+      Configure() command, and hence the CONFIGURING state
+    * **abort_invoked** and **abort_completed**: bookending the Abort()
+      command, and hence the ABORTING state
+    * **obsreset_invoked** and **obsreset_completed**: bookending the
+      ObsReset() command, and hence the OBSRESETTING state
+    * **restart_invoked** and **restart_completed**: bookending the
+      Restart() command, and hence the RESTARTING state
+    * **end_invoked**, **scan_invoked**, **end_scan_invoked**: these
+      result in reflexive transitions, and are purely there to indicate
+      states in which the End(), Scan() and EndScan() commands are
+      permitted to be run
+
+    The component-oriented actions are:
+
+    * **component_obsfault**: the monitored component has experienced an
+      observation fault
+    * **component_unresourced**: the monitored component has become
+      unresourced
+    * **component_resourced**: the monitored component has become
+      resourced
+    * **component_unconfigured**: the monitored component has become
+      unconfigured
+    * **component_configured**: the monitored component has become
+      configured
+    * **component_scanning**: the monitored component has started
+      scanning
+    * **component_not_scanning**: the monitored component has stopped
+      scanning
+
+    A diagram of the state machine is shown below. Reflexive transitions
+    and transitions to FAULT obs state are omitted to simplify the
+    diagram.
+
+    .. uml:: subarray_obs_state_machine.uml
+      :caption: Diagram of the subarray obs state machine
+
+    The following is a diagram of the state machine, automatically
+    generated from the code. Its equivalence to the diagram above
+    demonstrates that the implementation is faithful to the design.
+
+    .. figure:: _SubarrayObsStateMachine_autogenerated.png
+      :alt: Diagram of the subarray obs state machine, as implemented
+
+
+    """
+
+    def __init__(self, callback=None, **extra_kwargs):
+        """
+        Initialises the model.
+
+        :param callback: A callback to be called when the state changes
+        :type callback: callable
+        :param extra_kwargs: Additional keywords arguments to pass to super class
+            initialiser (useful for graphing)
+        """
+        self._callback = callback
+
+        states = [
+            "EMPTY",
+            "RESOURCING_EMPTY",  # device RESOURCING but component has not resources
+            "RESOURCING_IDLE",  # device RESOURCING and component has resources
+            "IDLE",
+            "CONFIGURING_IDLE",  # device CONFIGURING but component is unconfigured
+            "CONFIGURING_READY",  # device CONFIGURING and component is configured
+            "READY",
+            "SCANNING",
+            "ABORTING",
+            "ABORTED",
+            "RESETTING",
+            "RESTARTING",
+            "FAULT",
+        ]
+        transitions = [
+            {
+                "source": "*",
+                "trigger": "component_obsfault",
+                "dest": "FAULT",
+            },
+            {
+                "source": "EMPTY",
+                "trigger": "assign_invoked",
+                "dest": "RESOURCING_EMPTY",
+            },
+            {
+                "source": "EMPTY",
+                "trigger": "release_invoked",
+                "dest": "RESOURCING_EMPTY",
+            },
+            {
+                "source": "IDLE",
+                "trigger": "assign_invoked",
+                "dest": "RESOURCING_IDLE",
+            },
+            {
+                "source": "IDLE",
+                "trigger": "release_invoked",
+                "dest": "RESOURCING_IDLE",
+            },
+            {
+                "source": "RESOURCING_EMPTY",
+                "trigger": "component_resourced",
+                "dest": "RESOURCING_IDLE",
+            },
+            {
+                "source": "RESOURCING_IDLE",
+                "trigger": "component_unresourced",
+                "dest": "RESOURCING_EMPTY",
+            },
+            {
+                "source": "RESOURCING_EMPTY",
+                "trigger": "assign_completed",
+                "dest": "EMPTY",
+            },
+            {
+                "source": "RESOURCING_EMPTY",
+                "trigger": "release_completed",
+                "dest": "EMPTY",
+            },
+            {
+                "source": "RESOURCING_IDLE",
+                "trigger": "assign_completed",
+                "dest": "IDLE",
+            },
+            {
+                "source": "RESOURCING_IDLE",
+                "trigger": "release_completed",
+                "dest": "IDLE",
+            },
+            {
+                "source": "IDLE",
+                "trigger": "configure_invoked",
+                "dest": "CONFIGURING_IDLE",
+            },
+            {
+                "source": "CONFIGURING_IDLE",
+                "trigger": "configure_completed",
+                "dest": "IDLE",
+            },
+            {
+                "source": "READY",
+                "trigger": "configure_invoked",
+                "dest": "CONFIGURING_READY",
+            },
+            {
+                "source": "CONFIGURING_IDLE",
+                "trigger": "component_configured",
+                "dest": "CONFIGURING_READY",
+            },
+            {
+                "source": "CONFIGURING_READY",
+                "trigger": "configure_completed",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "end_invoked",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "component_unconfigured",
+                "dest": "IDLE",
+            },
+            {
+                "source": "READY",
+                "trigger": "scan_invoked",
+                "dest": "READY",
+            },
+            {
+                "source": "READY",
+                "trigger": "component_scanning",
+                "dest": "SCANNING",
+            },
+            {
+                "source": "SCANNING",
+                "trigger": "end_scan_invoked",
+                "dest": "SCANNING",
+            },
+            {
+                "source": "SCANNING",
+                "trigger": "component_not_scanning",
+                "dest": "READY",
+            },
+            {
+                "source": [
+                    "IDLE",
+                    "CONFIGURING_IDLE",
+                    "CONFIGURING_READY",
+                    "READY",
+                    "SCANNING",
+                    "RESETTING",
+                ],
+                "trigger": "abort_invoked",
+                "dest": "ABORTING",
+            },
+            # Aborting implies trying to stop the monitored component
+            # while it is doing something. Thus the monitored component
+            # may send some events while in aborting state.
+            {
+                "source": "ABORTING",
+                "trigger": "component_unconfigured",  # Abort() invoked on ObsReset()
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "component_configured",  # Configure() was just finishing
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "component_not_scanning",  # Aborting implies stopping scan
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "component_scanning",  # Abort() invoked as scan is starting
+                "dest": "ABORTING",
+            },
+            {
+                "source": "ABORTING",
+                "trigger": "abort_completed",
+                "dest": "ABORTED",
+            },
+            {
+                "source": ["ABORTED", "FAULT"],
+                "trigger": "obsreset_invoked",
+                "dest": "RESETTING",
+            },
+            {
+                "source": "RESETTING",
+                "trigger": "component_unconfigured",  # Resetting implies deconfiguring
+                "dest": "RESETTING",
+            },
+            {
+                "source": "RESETTING",
+                "trigger": "obsreset_completed",
+                "dest": "IDLE",
+            },
+            {
+                "source": ["ABORTED", "FAULT"],
+                "trigger": "restart_invoked",
+                "dest": "RESTARTING",
+            },
+            {
+                "source": "RESTARTING",
+                "trigger": "component_unconfigured",  # Restarting implies deconfiguring
+                "dest": "RESTARTING",
+            },
+            {
+                "source": "RESTARTING",
+                "trigger": "component_unresourced",  # Restarting implies releasing
+                "dest": "RESTARTING",
+            },
+            {
+                "source": "RESTARTING",
+                "trigger": "restart_completed",
+                "dest": "EMPTY",
+            },
+        ]
+
+        super().__init__(
+            states=states,
+            initial="EMPTY",
+            transitions=transitions,
+            after_state_change=self._state_changed,
+            **extra_kwargs,
+        )
+        self._state_changed()
+
+    def _state_changed(self):
+        """
+        State machine callback that is called every time the obs_state
+        changes. Responsible for ensuring that callbacks are called.
+        """
+        if self._callback is not None:
+            self._callback(self.state)
+
+
+class SubarrayObsStateModel(ObsStateModel):
+    """
+    Implements the observation state model for subarray
+
+    The model supports all of the states of the
+    :py:class:`ska_tango_base.control_model.ObsState` enum:
+
+    * **EMPTY**: the subarray is unresourced
+    * **RESOURCING**: the subarray is performing a resourcing operation
+    * **IDLE**: the subarray is resourced but unconfigured
+    * **CONFIGURING**: the subarray is performing a configuring
+      operation
+    * **READY**: the subarray is resourced and configured
+    * **SCANNING**: the subarray is scanning
+    * **ABORTING**: the subarray is aborting
+    * **ABORTED**: the subarray has aborted
+    * **RESETTING**: the subarray is resetting from an ABORTED or FAULT
+      state back to IDLE
+    * **RESTARTING**: the subarray is restarting from an ABORTED or
+      FAULT state back to EMPTY
+    * **FAULT**: the subarray has encountered a observation fault.
+
+    A diagram of the subarray observation state model is shown below.
+    This model is non-deterministic as diagrammed, but the underlying
+    state machines has extra states and transitions that render it
+    deterministic. This class simply maps those extra classes onto
+    valid ObsState values.
+
+    .. uml:: subarray_obs_state_model.uml
+       :caption: Diagram of the subarray observation state model
+    """
+
+    def __init__(self, logger, callback=None):
+        """
+        Initialises the model.
+
+        :param logger: the logger to be used by this state model.
+        :type logger: a logger that implements the standard library
+            logger interface
+        :param callback: A callback to be called when a transition
+            causes a change to device obs_state
+        :type callback: callable
+        """
+        super().__init__(_SubarrayObsStateMachine, logger, callback=callback)
+
+    _obs_state_mapping = {
+        "EMPTY": ObsState.EMPTY,
+        "RESOURCING_EMPTY": ObsState.RESOURCING,
+        "RESOURCING_IDLE": ObsState.RESOURCING,
+        "IDLE": ObsState.IDLE,
+        "CONFIGURING_IDLE": ObsState.CONFIGURING,
+        "CONFIGURING_READY": ObsState.CONFIGURING,
+        "READY": ObsState.READY,
+        "SCANNING": ObsState.SCANNING,
+        "ABORTING": ObsState.ABORTING,
+        "ABORTED": ObsState.ABORTED,
+        "RESETTING": ObsState.RESETTING,
+        "RESTARTING": ObsState.RESTARTING,
+        "FAULT": ObsState.FAULT,
+    }
+
+    def _obs_state_changed(self, machine_state):
+        """
+        Helper method that updates obs_state whenever the observation
+        state machine reports a change of state, ensuring that the
+        callback is called if one exists.
+
+        :param machine_state: the new state of the observation state
+            machine
+        :type machine_state: str
+        """
+        obs_state = self._obs_state_mapping[machine_state]
+        if self._obs_state != obs_state:
+            self._obs_state = obs_state
+            if self._callback is not None:
+                self._callback(obs_state)
diff --git a/src/ska_tango_base/utils.py b/src/ska_tango_base/utils.py
index d33b6fa6b7a5c61420b0d132bb815fbdb65153b9..2ed8c12eda2da37980ba4422f63bb11c50e39643 100644
--- a/src/ska_tango_base/utils.py
+++ b/src/ska_tango_base/utils.py
@@ -288,53 +288,73 @@ def get_groups_from_json(json_definitions):
 
     The general format of the list is as follows, with optional "devices" and
     "subgroups" keys:
-        [ {"group_name": "<name>",
-           "devices": ["<dev name>", ...]},
-          {"group_name": "<name>",
-           "devices": ["<dev name>", "<dev name>", ...],
-           "subgroups" : [{<nested group>},
-                          {<nested group>}, ...]},
-          ...
-          ]
+
+    .. code-block:: py
+
+        [
+            {"group_name": "<name>", "devices": ["<dev name>", ...]},
+            {
+                "group_name": "<name>",
+                "devices": ["<dev name>", "<dev name>", ...],
+                "subgroups" : [{<nested group>}, {<nested group>}, ...]
+            },
+            ...
+        ]
 
     For example, a hierarchy of racks, servers and switches:
-    [ {"group_name": "servers",
-       "devices": ["elt/server/1", "elt/server/2",
-                   "elt/server/3", "elt/server/4"]},
-      {"group_name": "switches",
-       "devices": ["elt/switch/A", "elt/switch/B"]},
-      {"group_name": "pdus",
-       "devices": ["elt/pdu/rackA", "elt/pdu/rackB"]},
-      {"group_name": "racks",
-       "subgroups": [
-            {"group_name": "rackA",
-             "devices": ["elt/server/1", "elt/server/2",
-                         "elt/switch/A", "elt/pdu/rackA"]},
-            {"group_name": "rackB",
-             "devices": ["elt/server/3", "elt/server/4",
-                         "elt/switch/B", "elt/pdu/rackB"],
-             "subgroups": []}
-       ]} ]
-
-
-    Parameters
-    ----------
-    json_definitions: sequence of str
-        Sequence of strings, each one a JSON dict with keys "group_name", and
-        one or both of:  "devices" and "subgroups", recursively defining
-        the hierarchy.
-
-    Returns
-    -------
-    groups: dict
-        The keys of the dict are the names of the groups, in the following form:
-            {"<group name 1>": <tango.Group>,
-             "<group name 2>": <tango.Group>, ...}.
-        Will be an empty dict if no groups were specified.
-
-    Raises
-    ------
-    GroupDefinitionsError:
+
+    .. code-block:: py
+
+        [
+            {
+                "group_name": "servers",
+                "devices": [
+                    "elt/server/1", "elt/server/2", "elt/server/3", "elt/server/4"
+                ]
+            },
+            {
+                "group_name": "switches",
+                "devices": ["elt/switch/A", "elt/switch/B"]
+            },
+            {
+                "group_name": "pdus",
+                "devices": ["elt/pdu/rackA", "elt/pdu/rackB"]
+            },
+            {
+                "group_name": "racks",
+                "subgroups": [
+                    {
+                        "group_name": "rackA",
+                        "devices": [
+                            "elt/server/1", "elt/server/2", "elt/switch/A", "elt/pdu/rackA"
+                        ]
+                    },
+                    {
+                        "group_name": "rackB",
+                        "devices": [
+                            "elt/server/3",
+                            "elt/server/4",
+                            "elt/switch/B",
+                            "elt/pdu/rackB"
+                        ],
+                        "subgroups": []
+                    }
+                ]
+            }
+        ]
+
+    :param json_definitions: Sequence of strings, each one a JSON dict
+        with keys "group_name", and one or both of:  "devices" and
+        "subgroups", recursively defining the hierarchy.
+    :type json_definitions: sequence of str
+
+    :return: A dictionary, the keys of which are the names of the
+        groups, in the following form: {"<group name 1>": <tango.Group>,
+        "<group name 2>": <tango.Group>, ...}. Will be an empty dict if
+        no groups were specified.
+    :rtype: dict
+
+    :raises GroupDefinitionsError:
         - If error parsing JSON string.
         - If missing keys in the JSON definition.
         - If invalid device name.
@@ -411,22 +431,19 @@ def _build_group(definition):
     return group
 
 
-def validate_capability_types(
-        command_name, requested_capabilities, valid_capabilities):
-    """Check the validity of the input parameter passed on to the command specified
-    by the command_name parameter.
-
-    Parameters
-    ----------
-    command_name: str
-        The name of the command which is to be executed.
-    requested_capabilities: list
-        A list of strings representing capability types.
-    valid_capabilities: list
-        A list of strings representing capability types.
-    Raises
-    ------
-    tango.DevFailed: If any of the capabilities requested are not valid.
+def validate_capability_types(command_name, requested_capabilities, valid_capabilities):
+    """
+    Check the validity of the input parameter passed on to the command
+    specified by the command_name parameter.
+
+    :param command_name: The name of the command to be executed.
+    :type command_name: str
+    :param requested_capabilities: A list of strings representing
+        capability types.
+    :type requested_capabilities: list(str)
+    :param valid_capabilities: A list of strings representing capability
+        types.
+    :type valid_capabilities: list(str)
     """
     invalid_capabilities = list(
         set(requested_capabilities) - set(valid_capabilities))
@@ -438,23 +455,18 @@ def validate_capability_types(
 
 
 def validate_input_sizes(command_name, argin):
-    """Check the validity of the input parameters passed on to the command specified
-    by the command_name parameter.
-
-    Parameters
-    ----------
-    command_name: str
-        The name of the command which is to be executed.
-    argin: tango.DevVarLongStringArray
-        A tuple of two lists.
-
-    Raises
-    ------
-    tango.DevFailed: If the two lists are not equal in length.
+    """
+    Check the validity of the input parameters passed on to the command
+    specified by the command_name parameter.
+
+    :param command_name: The name of the command which is to be executed.
+    :type command_name: str
+    :param argin: A tuple of two lists
+    :type argin: tango.DevVarLongStringArray
     """
     capabilities_instances, capability_types = argin
     if len(capabilities_instances) != len(capability_types):
-        Except.throw_exception("Command failed!", "Argin value lists size mismatch.",
+        Except.throw_exception("Command failed!", "Argin value lists size mismatch.", 
                                command_name, ErrSeverity.ERR)
 
 
@@ -479,11 +491,12 @@ def for_testing_only(func, _testing_check=lambda: 'pytest' in sys.modules):
     argument is inaccessible via the @-syntax, which is a nice bonus.)
     """
     @functools.wraps(func)
-    def wrapper(*args, **kwargs):
+    def _wrapper(*args, **kwargs):
         """
         Function wrapper for `testing_only` decorator.
         """
         if not _testing_check():
             warnings.warn(f"{func.__name__} should only be used for testing purposes.")
         return func(*args, **kwargs)
-    return wrapper
+    
+    return _wrapper
diff --git a/tests/conftest.py b/tests/conftest.py
index ca3efd3a9c8096dc370df7448c2804aeaf7ecebd..4039cb3036e0915ab9cd88b59ecf4985f90c5766 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,407 +1,35 @@
 """
 A module defining a list of fixtures that are shared across all ska_tango_base tests.
 """
-from collections import defaultdict
 import importlib
-import itertools
-import json
-import pytest
+import logging
 from queue import Empty, Queue
-from transitions import MachineError
 
-from tango import DevState, EventType
+import pytest
+from tango import EventType
 from tango.test_context import DeviceTestContext
 
-from ska_tango_base.control_model import AdminMode, ObsState
-from ska_tango_base.faults import StateModelError
-
-
-def pytest_configure(config):
-    """
-    pytest hook, used here to register custom "state_machine_tester" marks
-    """
-    config.addinivalue_line(
-        "markers",
-        "state_machine_tester: indicate that this class is state machine "
-        "tester class, and tests should be parameterised by the states and "
-        "actions in the specification provided in its argument."
-    )
-
-
-def pytest_generate_tests(metafunc):
-    """
-    pytest hook that generates tests; this hook ensures that any test
-    class that is marked with the `state_machine_tester` custom marker
-    will have its tests parameterised by the states and actions in the
-    specification provided by that mark
-    """
-    mark = metafunc.definition.get_closest_marker("state_machine_tester")
-    if mark:
-        spec = mark.args[0]
-
-        states = {state: spec["states"][state] or state for state in spec["states"]}
-
-        triggers = set()
-        expected = defaultdict(lambda: None)
-        for transition in spec["transitions"]:
-            triggers.add(transition["trigger"])
-            expected[(transition["from"], transition["trigger"])
-                     ] = states[transition["to"]]
-        test_cases = list(itertools.product(sorted(states), sorted(triggers)))
-        test_ids = [f"{state}-{trigger}" for (state, trigger) in test_cases]
-
-        metafunc.parametrize(
-            "state_under_test, action_under_test, expected_state",
-            [
-                (
-                    states[state],
-                    trigger,
-                    expected[(state, trigger)]
-                ) for (state, trigger) in test_cases
-            ],
-            ids=test_ids
-        )
-
-
-class StateMachineTester:
-    """
-    Abstract base class for a class for testing state machines
-    """
-
-    def test_state_machine(
-        self, machine, state_under_test, action_under_test, expected_state,
-    ):
-        """
-        Implements the unit test for a state machine: for a given
-        initial state and an action, does execution of that action, from
-        that state, yield the expected results? If the action was
-        allowed from that state, does the machine transition to the
-        correct state? If the action was not allowed from that state,
-        does the machine reject the action (e.g. raise an exception or
-        return an error code) and remain in the current state?
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state_under_test: the state from which the
-            `action_under_test` is being tested
-        :type state_under_test: string
-        :param action_under_test: the action being tested from the
-            `state_under_test`
-        :type action_under_test: string
-        :param expected_state: the state to which the machine is
-            expected to transition, as a result of performing the
-            `action_under_test` in the `state_under_test`. If None, then
-            the action should be disallowed and result in no change of
-            state.
-        :type expected_state: string
-
-        """
-        # Put the device into the state under test
-        self.to_state(machine, state_under_test)
-
-        # Check that we are in the state under test
-        self.assert_state(machine, state_under_test)
-
-        # Test that the action under test does what we expect it to
-        if expected_state is None:
-            # Action should fail and the state should not change
-            assert not self.is_action_allowed(machine, action_under_test)
-            self.check_action_fails(machine, action_under_test)
-            self.assert_state(machine, state_under_test)
-        else:
-            # Action should succeed
-            assert self.is_action_allowed(machine, action_under_test)
-            self.perform_action(machine, action_under_test)
-            self.assert_state(machine, expected_state)
-
-    def assert_state(self, machine, state):
-        """
-        Abstract method for asserting the current state of the state
-        machine under test
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: string
-        """
-        raise NotImplementedError()
-
-    def is_action_allowed(self, machine, action):
-        """
-        Abstract method for checking whether the action under test is
-        allowed from the current state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        raise NotImplementedError()
-
-    def perform_action(self, machine, action):
-        """
-        Abstract method for performing an action on the state machine
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        raise NotImplementedError()
-
-    def check_action_fails(self, machine, action):
-        """
-        Abstract method for asserting that an action fails if performed
-        on the state machine under test in its current state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        raise NotImplementedError()
-
-    def to_state(self, machine, target_state):
-        """
-        Abstract method for getting the state machine into a target
-        state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param target_state: the state that we want to get the state
-            machine under test into
-        :type target_state: str
-        """
-        raise NotImplementedError()
-
-
-class TransitionsStateMachineTester(StateMachineTester):
-    """
-    Concrete implementation of a StateMachineTester for a pytransitions
-    state machine (with autotransitions turned on). The states and
-    actions in the state machine specification must correspond exactly
-    with the machine's states and triggers.
-    """
-
-    def assert_state(self, machine, state):
-        """
-        Assert the current state of the state machine under test.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: str
-        """
-        assert machine.state == state
-
-    def is_action_allowed(self, machine, action):
-        """
-        Check whether the action under test is allowed from the current
-        state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        return action in machine.get_triggers(machine.state)
-
-    def perform_action(self, machine, action):
-        """
-        Perform a given action on the state machine under test.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        machine.trigger(action)
-
-    def check_action_fails(self, machine, action):
-        """
-        Check that attempting a given action on the state machine under
-        test fails in its current state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        with pytest.raises(MachineError):
-            self.perform_action(machine, action)
-
-    def to_state(self, machine, target_state):
-        """
-        Transition the state machine to a target state. This
-        implementation uses autotransitions. If the pytransitions state
-        machine under test has autotransitions turned off, then this
-        method will need to be overridden by some other method of
-        putting the machine into the state under test.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param target_state: the state that we want to get the state
-            machine under test into
-        :type target_state: str
-        """
-        machine.trigger(f"to_{target_state}")
-
-
-class ModelStateMachineTester(StateMachineTester):
-    """
-    Abstract base class for testing state models using state machines.
-
-    The ``assert_state`` method has to be implemented in concrete classes,
-    and the `machine` fixture must also be provided.
-    """
-
-    def assert_state(self, machine, state):
-        """
-        Assert the current state of this state machine, based on the
-        values of the adminMode, opState and obsState attributes of this
-        model.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: dict
-        """
-        raise NotImplementedError()
-
-    def is_action_allowed(self, machine, action):
-        """
-        Returns whether the state machine under test thinks an action
-        is permitted in its current state
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        return machine.is_action_allowed(action)
-
-    def perform_action(self, machine, action):
-        """
-        Perform a given action on the state machine under test.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        machine.perform_action(action)
 
-    def check_action_fails(self, machine, action):
-        """
-        Assert that performing a given action on the state maching under
-        test fails in its current state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param action: action to be performed on the state machine
-        :type action: str
-        """
-        with pytest.raises(StateModelError):
-            self.perform_action(machine, action)
-
-    def to_state(self, machine, target_state):
-        """
-        Transition the state machine to a target state.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param target_state: the state that we want to get the state
-            machine under test into
-        :type target_state: dict
-        """
-        machine._straight_to_state(**target_state)
-
-
-def load_data(name):
+@pytest.fixture(scope="class")
+def device_properties():
     """
-    Loads a dataset by name. This implementation uses the name to find a
-    JSON file containing the data to be loaded.
-
-    :param name: name of the dataset to be loaded; this implementation
-        uses the name to find a JSON file containing the data to be
-        loaded.
-    :type name: string
+    Fixture that returns device_properties to be provided to the
+    device under test. This is a default implementiong that provides
+    no properties.
     """
-    with open(f"tests/data/{name}.json", "r") as json_file:
-        return json.load(json_file)
-
+    return {}
 
-def load_state_machine_spec(name):
+@pytest.fixture(scope="class")
+def tango_context(device_test_config):
     """
-    Loads a state machine specification by name.
-
-    :param name: name of the dataset to be loaded; this implementation
-        uses the name to find a JSON file containing the data to be
-        loaded.
-    :type name: string
+    Fixture that returns a Tango DeviceTestContext object, in which the
+    device under test is running.
     """
-    machine_spec = load_data(name)
-    for state in machine_spec["states"]:
-        state_spec = machine_spec["states"][state]
-        if "admin_mode" in state_spec:
-            state_spec["admin_mode"] = AdminMode[state_spec["admin_mode"]]
-        if "op_state" in state_spec:
-            state_spec["op_state"] = getattr(DevState, state_spec["op_state"])
-        if "obs_state" in state_spec:
-            state_spec["obs_state"] = ObsState[state_spec["obs_state"]]
-    return machine_spec
-
+    component_manager_patch = device_test_config.pop("component_manager_patch", None)
+    if component_manager_patch is not None:
+        device_test_config["device"].create_component_manager = component_manager_patch
 
-@pytest.fixture(scope="class")
-def tango_context(request):
-    """Creates and returns a TANGO DeviceTestContext object.
-
-    Parameters
-    ----------
-    request: _pytest.fixtures.SubRequest
-        A request object gives access to the requesting test context.
-    """
-    test_properties = {
-        'SKAMaster': {
-            'SkaLevel': '4',
-            'LoggingTargetsDefault': '',
-            'GroupDefinitions': '',
-            'NrSubarrays': '16',
-            'CapabilityTypes': '',
-            'MaxCapabilities': ['BAND1:1', 'BAND2:1']
-        },
-        'SKASubarray': {
-            "CapabilityTypes": ["BAND1", "BAND2"],
-            'LoggingTargetsDefault': '',
-            'GroupDefinitions': '',
-            'SkaLevel': '4',
-            'SubID': '1',
-        },
-        'CspSubElementMaster': {
-            'PowerDelayStandbyOn': '1.5',
-            'PowerDelayStandbyOff': '1.0',
-        },
-
-        'CspSubElementObsDevice': {
-            'DeviceID': '11',
-        },
-    }
-
-    # This fixture is used to decorate classes like "TestSKABaseDevice" or
-    # "TestSKALogger". We drop the first "Test" from the string to get the
-    # class name of the device under test.
-    # Similarly, the test module is like "test_base_device.py".  We drop the
-    # first "test_" to get the module name
-    test_class_name = request.cls.__name__
-    class_name = test_class_name.split('Test', 1)[-1]
-    module = importlib.import_module("ska_tango_base", class_name)
-    class_type = getattr(module, class_name)
-
-    tango_context = DeviceTestContext(
-        class_type, properties=test_properties.get(class_name))
+    tango_context = DeviceTestContext(**device_test_config)
     tango_context.start()
     yield tango_context
     tango_context.stop()
@@ -426,9 +54,9 @@ def tango_change_event_helper(tango_context):
     method with the name of the attribute for which you want change events.
     The returned value will be a callback handler that you can interrogate
     with ``assert_not_called``, ``assert_call``, ``assert_calls``, and
-    ``value`` methods.::
+    ``value`` methods.
 
-    .. code-block:: python
+    .. code-block:: py
 
         state_callback = tango_change_event_helper.subscribe("state")
         state_callback.assert_call(DevState.OFF)
@@ -566,3 +194,11 @@ def tango_change_event_helper(tango_context):
                 self.assert_call(value)
 
     yield _Callback
+
+
+@pytest.fixture()
+def logger():
+    """
+    Fixture that returns a default logger for tests
+    """
+    return logging.Logger("Test logger")
diff --git a/tests/data/csp_subelement_obsdev_transitions.json b/tests/data/csp_subelement_obsdev_transitions.json
deleted file mode 100644
index e0f891cfb7b3e8c892cc0bb467150ea370c77bed..0000000000000000000000000000000000000000
--- a/tests/data/csp_subelement_obsdev_transitions.json
+++ /dev/null
@@ -1,158 +0,0 @@
-{
-    "states": {
-        "FAULT": {},
-        "IDLE": {},
-        "CONFIGURING": {},
-        "ABORTING": {},
-        "READY": {},
-        "SCANNING": {},
-        "ABORTED": {}
-    },
-    "transitions": [
-        {
-            "from": "IDLE",
-            "to": "CONFIGURING",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE",
-            "to": "IDLE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "READY",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "FAULT",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY",
-            "to": "IDLE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY",
-            "to": "FAULT",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY",
-            "to": "CONFIGURING",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY",
-            "to": "SCANNING",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING",
-            "to": "READY",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING",
-            "to": "READY",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING",
-            "to": "ABORTED",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING",
-            "to": "FAULT",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED",
-            "to": "FAULT",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "ABORTED",
-            "to": "IDLE",
-            "trigger": "reset_succeeded"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT",
-            "to": "IDLE",
-            "trigger": "reset_succeeded"
-        }
-    ]
-}
diff --git a/tests/data/csp_subelement_obsdevice_state_machine.json b/tests/data/csp_subelement_obsdevice_state_machine.json
deleted file mode 100644
index 65b7d49788d3bf1cb549c05719ca38fc1b77b07e..0000000000000000000000000000000000000000
--- a/tests/data/csp_subelement_obsdevice_state_machine.json
+++ /dev/null
@@ -1,1446 +0,0 @@
-{
-    "states": {
-        "INIT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "INIT",
-            "obs_state": "IDLE"
-        },
-        "INIT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "INIT",
-            "obs_state": "IDLE"
-        },
-        "INIT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "INIT",
-            "obs_state": "IDLE"
-        },
-        "INIT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "INIT",
-            "obs_state": "IDLE"
-        },
-        "INIT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "INIT",
-            "obs_state": "IDLE"
-        },
-        "FAULT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "FAULT",
-            "obs_state": "IDLE"
-        },
-        "FAULT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "FAULT",
-            "obs_state": "IDLE"
-        },
-        "FAULT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "FAULT",
-            "obs_state": "IDLE"
-        },
-        "FAULT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "FAULT",
-            "obs_state": "IDLE"
-        },
-        "FAULT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "FAULT",
-            "obs_state": "IDLE"
-        },
-        "DISABLE_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "DISABLE",
-            "obs_state": "IDLE"
-        },
-        "DISABLE_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "DISABLE",
-            "obs_state": "IDLE"
-        },
-        "DISABLE_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "DISABLE",
-            "obs_state": "IDLE"
-        },
-        "DISABLE_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "DISABLE",
-            "obs_state": "IDLE"
-        },
-        "DISABLE_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "DISABLE",
-            "obs_state": "IDLE"
-        },
-        "STANDBY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "STANDBY",
-            "obs_state": "IDLE"
-        },
-        "STANDBY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "STANDBY",
-            "obs_state": "IDLE"
-        },
-        "OFF_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "OFF",
-            "obs_state": "IDLE"
-        },
-        "OFF_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "OFF",
-            "obs_state": "IDLE"
-        },
-        "IDLE_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "IDLE"
-        },
-        "IDLE_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "IDLE"
-        },
-        "CONFIGURING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "CONFIGURING"
-        },
-        "CONFIGURING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "CONFIGURING"
-        },
-        "READY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "READY"
-        },
-        "READY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "READY"
-        },
-        "SCANNING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "SCANNING"
-        },
-        "SCANNING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "SCANNING"
-        },
-        "ABORTING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "ABORTING"
-        },
-        "ABORTING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "ABORTING"
-        },
-        "ABORTED_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "ABORTED"
-        },
-        "ABORTED_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "ABORTED"
-        },
-        "OBSFAULT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "FAULT"
-        },
-        "OBSFAULT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "FAULT"
-        }
-    },
-    "transitions": [
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "SCANNING_ONLINE",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "SCANNING_MAINTENANCE",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "SCANNING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "SCANNING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "ABORTED_ONLINE",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "ABORTED_MAINTENANCE",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "ABORTED_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "ABORTED_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        }
-    ]
-}
diff --git a/tests/data/device_state_machine.json b/tests/data/device_state_machine.json
deleted file mode 100644
index 544064bcc27c287d0574895a5777d7d06cf72481..0000000000000000000000000000000000000000
--- a/tests/data/device_state_machine.json
+++ /dev/null
@@ -1,860 +0,0 @@
-{
-    "states": {
-        "INIT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "INIT"
-        },
-        "INIT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "INIT"
-        },
-        "INIT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "INIT"
-        },
-        "INIT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "INIT"
-        },
-        "INIT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "INIT"
-        },
-        "FAULT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "FAULT"
-        },
-        "FAULT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "FAULT"
-        },
-        "FAULT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "FAULT"
-        },
-        "FAULT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "FAULT"
-        },
-        "FAULT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "FAULT"
-        },
-        "DISABLE_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "DISABLE"
-        },
-        "DISABLE_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "DISABLE"
-        },
-        "DISABLE_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "DISABLE"
-        },
-        "DISABLE_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "DISABLE"
-        },
-        "DISABLE_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "DISABLE"
-        },
-        "STANDBY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "STANDBY"
-        },
-        "STANDBY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "STANDBY"
-        },
-        "OFF_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "OFF"
-        },
-        "OFF_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "OFF"
-        },
-        "ON_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON"
-        },
-        "ON_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON"
-        }
-    },
-    "transitions": [
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "ON_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "ON_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ON_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ON_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ON_MAINTENANCE",
-            "to": "ON_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ON_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ON_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ON_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ON_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ON_ONLINE",
-            "to": "ON_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ON_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ON_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        }
-    ]
-}
\ No newline at end of file
diff --git a/tests/data/observation_state_machine.json b/tests/data/observation_state_machine.json
deleted file mode 100644
index 5315193a1a9bf9e96024cc78b683cfb7c94c33fd..0000000000000000000000000000000000000000
--- a/tests/data/observation_state_machine.json
+++ /dev/null
@@ -1,232 +0,0 @@
-{
-    "states": {
-        "EMPTY": {},
-        "RESOURCING": {},
-        "FAULT": {},
-        "IDLE": {},
-        "CONFIGURING": {},
-        "ABORTING": {},
-        "READY": {},
-        "SCANNING": {},
-        "ABORTED": {},
-        "RESETTING": {},
-        "RESTARTING": {}
-    },
-    "transitions": [
-        {
-            "from": "EMPTY",
-            "to": "RESOURCING",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "EMPTY",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESOURCING",
-            "to": "IDLE",
-            "trigger": "resourcing_succeeded_some_resources"
-        },
-        {
-            "from": "RESOURCING",
-            "to": "EMPTY",
-            "trigger": "resourcing_succeeded_no_resources"
-        },
-        {
-            "from": "RESOURCING",
-            "to": "FAULT",
-            "trigger": "resourcing_failed"
-        },
-        {
-            "from": "RESOURCING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE",
-            "to": "RESOURCING",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "RESOURCING",
-            "trigger": "release_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "CONFIGURING",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "READY",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "FAULT",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY",
-            "to": "IDLE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY",
-            "to": "FAULT",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY",
-            "to": "CONFIGURING",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY",
-            "to": "SCANNING",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING",
-            "to": "READY",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING",
-            "to": "READY",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING",
-            "to": "ABORTED",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING",
-            "to": "FAULT",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED",
-            "to": "RESETTING",
-            "trigger": "reset_started"
-        },
-        {
-            "from": "ABORTED",
-            "to": "RESTARTING",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "ABORTED",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT",
-            "to": "RESETTING",
-            "trigger": "reset_started"
-        },
-        {
-            "from": "FAULT",
-            "to": "RESTARTING",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESETTING",
-            "to": "IDLE",
-            "trigger": "reset_succeeded"
-        },
-        {
-            "from": "RESETTING",
-            "to": "FAULT",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "RESETTING",
-            "to": "ABORTING",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "RESETTING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESTARTING",
-            "to": "EMPTY",
-            "trigger": "restart_succeeded"
-        },
-        {
-            "from": "RESTARTING",
-            "to": "FAULT",
-            "trigger": "restart_failed"
-        },
-        {
-            "from": "RESTARTING",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        }
-    ]
-}
\ No newline at end of file
diff --git a/tests/data/operation_state_machine.json b/tests/data/operation_state_machine.json
deleted file mode 100644
index 86774be77bf13c6c47e4ba4deb90401316c4d207..0000000000000000000000000000000000000000
--- a/tests/data/operation_state_machine.json
+++ /dev/null
@@ -1,316 +0,0 @@
-{
-    "states": {
-        "UNINITIALISED": {},
-        "INIT": {},
-        "DISABLE": {},
-        "INIT_ADMIN": {},
-        "DISABLE_ADMIN": {},
-        "STANDBY": {},
-        "OFF": {},
-        "FAULT": {},
-        "FAULT_ADMIN": {},
-        "ON": {}
-    },
-    "transitions": [
-        {
-            "from": "UNINITIALISED",
-            "to": "INIT",
-            "trigger": "init_started"
-        },
-        {
-            "from": "INIT",
-            "to": "DISABLE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_ADMIN",
-            "to": "DISABLE_ADMIN",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT",
-            "to": "STANDBY",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT",
-            "to": "OFF",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT",
-            "to": "FAULT",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT",
-            "to": "INIT_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "INIT",
-            "to": "INIT",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "INIT_ADMIN",
-            "to": "INIT_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "INIT_ADMIN",
-            "to": "INIT",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "INIT_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT",
-            "to": "DISABLE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_ADMIN",
-            "to": "DISABLE_ADMIN",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT",
-            "to": "STANDBY",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT",
-            "to": "OFF",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "FAULT",
-            "to": "FAULT",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "FAULT_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "FAULT_ADMIN",
-            "to": "FAULT",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "DISABLE",
-            "to": "DISABLE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE",
-            "to": "FAULT",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE",
-            "to": "STANDBY",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE",
-            "to": "FAULT",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE",
-            "to": "OFF",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE",
-            "to": "FAULT",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE",
-            "to": "DISABLE_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "DISABLE",
-            "to": "DISABLE",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "DISABLE_ADMIN",
-            "to": "DISABLE_ADMIN",
-            "trigger": "admin_on"
-        },
-        {
-            "from": "DISABLE_ADMIN",
-            "to": "DISABLE",
-            "trigger": "admin_off"
-        },
-        {
-            "from": "DISABLE_ADMIN",
-            "to": "DISABLE_ADMIN",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_ADMIN",
-            "to": "FAULT_ADMIN",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY",
-            "to": "DISABLE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY",
-            "to": "FAULT",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY",
-            "to": "STANDBY",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY",
-            "to": "FAULT",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY",
-            "to": "OFF",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY",
-            "to": "FAULT",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF",
-            "to": "DISABLE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF",
-            "to": "FAULT",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF",
-            "to": "STANDBY",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF",
-            "to": "FAULT",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF",
-            "to": "OFF",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF",
-            "to": "FAULT",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF",
-            "to": "ON",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OFF",
-            "to": "FAULT",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ON",
-            "to": "OFF",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ON",
-            "to": "FAULT",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ON",
-            "to": "ON",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ON",
-            "to": "FAULT",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ON",
-            "to": "FAULT",
-            "trigger": "fatal_error"
-        }
-    ]
-}
\ No newline at end of file
diff --git a/tests/data/subarray_state_machine.json b/tests/data/subarray_state_machine.json
deleted file mode 100644
index 57e33c3fee357176780044d46c487cfd3f7202a9..0000000000000000000000000000000000000000
--- a/tests/data/subarray_state_machine.json
+++ /dev/null
@@ -1,1801 +0,0 @@
-{
-    "states": {
-        "INIT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "INIT",
-            "obs_state": "EMPTY"
-        },
-        "INIT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "INIT",
-            "obs_state": "EMPTY"
-        },
-        "INIT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "INIT",
-            "obs_state": "EMPTY"
-        },
-        "INIT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "INIT",
-            "obs_state": "EMPTY"
-        },
-        "INIT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "INIT",
-            "obs_state": "EMPTY"
-        },
-        "FAULT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "FAULT",
-            "obs_state": "EMPTY"
-        },
-        "FAULT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "FAULT",
-            "obs_state": "EMPTY"
-        },
-        "FAULT_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "FAULT",
-            "obs_state": "EMPTY"
-        },
-        "FAULT_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "FAULT",
-            "obs_state": "EMPTY"
-        },
-        "FAULT_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "FAULT",
-            "obs_state": "EMPTY"
-        },
-        "DISABLE_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "DISABLE",
-            "obs_state": "EMPTY"
-        },
-        "DISABLE_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "DISABLE",
-            "obs_state": "EMPTY"
-        },
-        "DISABLE_OFFLINE": {
-            "admin_mode": "OFFLINE",
-            "op_state": "DISABLE",
-            "obs_state": "EMPTY"
-        },
-        "DISABLE_NOTFITTED": {
-            "admin_mode": "NOT_FITTED",
-            "op_state": "DISABLE",
-            "obs_state": "EMPTY"
-        },
-        "DISABLE_RESERVED": {
-            "admin_mode": "RESERVED",
-            "op_state": "DISABLE",
-            "obs_state": "EMPTY"
-        },
-        "STANDBY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "STANDBY",
-            "obs_state": "EMPTY"
-        },
-        "STANDBY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "STANDBY",
-            "obs_state": "EMPTY"
-        },
-        "OFF_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "OFF",
-            "obs_state": "EMPTY"
-        },
-        "OFF_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "OFF",
-            "obs_state": "EMPTY"
-        },
-        "EMPTY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "EMPTY"
-        },
-        "EMPTY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "EMPTY"
-        },
-        "RESOURCING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "RESOURCING"
-        },
-        "RESOURCING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "RESOURCING"
-        },
-        "IDLE_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "IDLE"
-        },
-        "IDLE_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "IDLE"
-        },
-        "CONFIGURING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "CONFIGURING"
-        },
-        "CONFIGURING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "CONFIGURING"
-        },
-        "READY_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "READY"
-        },
-        "READY_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "READY"
-        },
-        "SCANNING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "SCANNING"
-        },
-        "SCANNING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "SCANNING"
-        },
-        "ABORTING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "ABORTING"
-        },
-        "ABORTING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "ABORTING"
-        },
-        "ABORTED_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "ABORTED"
-        },
-        "ABORTED_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "ABORTED"
-        },
-        "OBSFAULT_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "FAULT"
-        },
-        "OBSFAULT_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "FAULT"
-        },
-        "RESETTING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "RESETTING"
-        },
-        "RESETTING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "RESETTING"
-        },
-        "RESTARTING_MAINTENANCE": {
-            "admin_mode": "MAINTENANCE",
-            "op_state": "ON",
-            "obs_state": "RESTARTING"
-        },
-        "RESTARTING_ONLINE": {
-            "admin_mode": "ONLINE",
-            "op_state": "ON",
-            "obs_state": "RESTARTING"
-        }
-    },
-    "transitions": [
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "init_succeeded_standby"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "init_succeeded_off"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "INIT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "init_succeeded_disable"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "init_failed"
-        },
-        {
-            "from": "INIT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "reset_succeeded_standby"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "reset_succeeded_off"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "reset_succeeded_disable"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "reset_failed"
-        },
-        {
-            "from": "FAULT_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "DISABLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "to_maintenance"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "to_online"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_OFFLINE",
-            "to": "FAULT_OFFLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_NOTFITTED",
-            "to": "FAULT_NOTFITTED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "to_reserved"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_NOTFITTED",
-            "trigger": "to_notfitted"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_OFFLINE",
-            "trigger": "to_offline"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "DISABLE_RESERVED",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "DISABLE_RESERVED",
-            "to": "FAULT_RESERVED",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "STANDBY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "DISABLE_MAINTENANCE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "STANDBY_MAINTENANCE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "EMPTY_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "DISABLE_ONLINE",
-            "trigger": "disable_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "disable_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "STANDBY_ONLINE",
-            "trigger": "standby_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "standby_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "EMPTY_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OFF_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "EMPTY_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "EMPTY_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "EMPTY_ONLINE",
-            "to": "RESOURCING_ONLINE",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "EMPTY_MAINTENANCE",
-            "to": "RESOURCING_MAINTENANCE",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "RESOURCING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "resourcing_succeeded_some_resources"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "EMPTY_ONLINE",
-            "trigger": "resourcing_succeeded_no_resources"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "resourcing_failed"
-        },
-        {
-            "from": "RESOURCING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "RESOURCING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "resourcing_succeeded_some_resources"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "EMPTY_MAINTENANCE",
-            "trigger": "resourcing_succeeded_no_resources"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "resourcing_failed"
-        },
-        {
-            "from": "RESOURCING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "RESOURCING_ONLINE",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "RESOURCING_ONLINE",
-            "trigger": "release_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "RESOURCING_MAINTENANCE",
-            "trigger": "assign_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "RESOURCING_MAINTENANCE",
-            "trigger": "release_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "IDLE_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "configure_succeeded"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "configure_failed"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "CONFIGURING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "CONFIGURING_ONLINE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "SCANNING_ONLINE",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "end_succeeded"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "end_failed"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "CONFIGURING_MAINTENANCE",
-            "trigger": "configure_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "SCANNING_MAINTENANCE",
-            "trigger": "scan_started"
-        },
-        {
-            "from": "READY_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "SCANNING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "READY_ONLINE",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "SCANNING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "scan_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "scan_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "READY_MAINTENANCE",
-            "trigger": "end_scan_succeeded"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "end_scan_failed"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "SCANNING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "ABORTED_ONLINE",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "ABORTED_MAINTENANCE",
-            "trigger": "abort_succeeded"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "abort_failed"
-        },
-        {
-            "from": "ABORTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "ABORTED_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "RESETTING_ONLINE",
-            "trigger": "obs_reset_started"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "RESTARTING_ONLINE",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "ABORTED_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "ABORTED_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "RESETTING_MAINTENANCE",
-            "trigger": "obs_reset_started"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "RESTARTING_MAINTENANCE",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "ABORTED_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "RESETTING_ONLINE",
-            "trigger": "obs_reset_started"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "RESTARTING_ONLINE",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "OBSFAULT_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "RESETTING_MAINTENANCE",
-            "trigger": "obs_reset_started"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "RESTARTING_MAINTENANCE",
-            "trigger": "restart_started"
-        },
-        {
-            "from": "OBSFAULT_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "RESETTING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "IDLE_ONLINE",
-            "trigger": "obs_reset_succeeded"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "ABORTING_ONLINE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "RESETTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "RESETTING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "IDLE_MAINTENANCE",
-            "trigger": "obs_reset_succeeded"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "obs_reset_failed"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "ABORTING_MAINTENANCE",
-            "trigger": "abort_started"
-        },
-        {
-            "from": "RESETTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "OFF_ONLINE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "RESTARTING_ONLINE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "FAULT_ONLINE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "EMPTY_ONLINE",
-            "trigger": "restart_succeeded"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "restart_failed"
-        },
-        {
-            "from": "RESTARTING_ONLINE",
-            "to": "OBSFAULT_ONLINE",
-            "trigger": "fatal_error"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "OFF_MAINTENANCE",
-            "trigger": "off_succeeded"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "off_failed"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "RESTARTING_MAINTENANCE",
-            "trigger": "on_succeeded"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "FAULT_MAINTENANCE",
-            "trigger": "on_failed"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "EMPTY_MAINTENANCE",
-            "trigger": "restart_succeeded"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "restart_failed"
-        },
-        {
-            "from": "RESTARTING_MAINTENANCE",
-            "to": "OBSFAULT_MAINTENANCE",
-            "trigger": "fatal_error"
-        }
-    ]
-}
\ No newline at end of file
diff --git a/tests/state/__init__.py b/tests/state/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/state/conftest.py b/tests/state/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb1a27e376735c0f7182a5526922bb5452ef8565
--- /dev/null
+++ b/tests/state/conftest.py
@@ -0,0 +1,352 @@
+"""
+A module defining a list of fixtures that are shared across all ska_tango_base tests.
+"""
+from collections import defaultdict
+import itertools
+import json
+import pytest
+from transitions import MachineError
+
+from tango import DevState
+
+from ska_tango_base.control_model import AdminMode, ObsState
+from ska_tango_base.faults import StateModelError
+
+
+def pytest_configure(config):
+    """
+    pytest hook, used here to register custom "state_machine_tester" marks
+    """
+    config.addinivalue_line(
+        "markers",
+        "state_machine_tester: indicate that this class is state machine "
+        "tester class, and tests should be parameterised by the states and "
+        "actions in the specification provided in its argument.",
+    )
+
+
+def pytest_generate_tests(metafunc):
+    """
+    pytest hook that generates tests; this hook ensures that any test
+    class that is marked with the `state_machine_tester` custom marker
+    will have its tests parameterised by the states and actions in the
+    specification provided by that mark
+    """
+    mark = metafunc.definition.get_closest_marker("state_machine_tester")
+    if mark:
+        spec = mark.args[0]
+
+        states = {state: spec["states"][state] or state for state in spec["states"]}
+
+        triggers = set()
+        expected = defaultdict(lambda: None)
+        for transition in spec["transitions"]:
+            triggers.add(transition["trigger"])
+            expected[(transition["from"], transition["trigger"])] = states[
+                transition["to"]
+            ]
+        test_cases = list(itertools.product(sorted(states), sorted(triggers)))
+        test_ids = [f"{state}-{trigger}" for (state, trigger) in test_cases]
+
+        metafunc.parametrize(
+            "state_under_test, action_under_test, expected_state",
+            [
+                (states[state], trigger, expected[(state, trigger)])
+                for (state, trigger) in test_cases
+            ],
+            ids=test_ids,
+        )
+
+
+class StateMachineTester:
+    """
+    Abstract base class for a class for testing state machines
+    """
+
+    def test_state_machine(
+        self,
+        machine_under_test,
+        state_under_test,
+        action_under_test,
+        expected_state,
+    ):
+        """
+        Implements the unit test for a state machine: for a given
+        initial state and an action, does execution of that action, from
+        that state, yield the expected results? If the action was
+        allowed from that state, does the machine transition to the
+        correct state? If the action was not allowed from that state,
+        does the machine reject the action (e.g. raise an exception or
+        return an error code) and remain in the current state?
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param state_under_test: the state from which the
+            `action_under_test` is being tested
+        :type state_under_test: string
+        :param action_under_test: the action being tested from the
+            `state_under_test`
+        :type action_under_test: string
+        :param expected_state: the state to which the machine is
+            expected to transition, as a result of performing the
+            `action_under_test` in the `state_under_test`. If None, then
+            the action should be disallowed and result in no change of
+            state.
+        :type expected_state: string
+
+        """
+        # Put the device into the state under test
+        self.to_state(machine_under_test, state_under_test)
+
+        # Check that we are in the state under test
+        self.assert_state(machine_under_test, state_under_test)
+
+        # Test that the action under test does what we expect it to
+        if expected_state is None:
+            # Action should fail and the state should not change
+            assert not self.is_action_allowed(machine_under_test, action_under_test)
+            self.check_action_fails(machine_under_test, action_under_test)
+            self.assert_state(machine_under_test, state_under_test)
+        else:
+            # Action should succeed
+            assert self.is_action_allowed(machine_under_test, action_under_test)
+            self.perform_action(machine_under_test, action_under_test)
+            self.assert_state(machine_under_test, expected_state)
+
+    def assert_state(self, machine_under_test, state):
+        """
+        Abstract method for asserting the current state of the state
+        machine under test
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param state: the state that we are asserting to be the current
+            state of the state machine under test
+        :type state: string
+        """
+        raise NotImplementedError()
+
+    def is_action_allowed(self, machine_under_test, action):
+        """
+        Abstract method for checking whether the action under test is
+        allowed from the current state.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        raise NotImplementedError()
+
+    def perform_action(self, machine_under_test, action):
+        """
+        Abstract method for performing an action on the state machine
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        raise NotImplementedError()
+
+    def check_action_fails(self, machine_under_test, action):
+        """
+        Abstract method for asserting that an action fails if performed
+        on the state machine under test in its current state.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        raise NotImplementedError()
+
+    def to_state(self, machine_under_test, target_state):
+        """
+        Abstract method for getting the state machine into a target
+        state.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param target_state_kwargs: specification of the target state
+            of the machine under test
+        :type target_state_kwargs: Any
+        """
+        raise NotImplementedError()
+
+
+class TransitionsStateMachineTester(StateMachineTester):
+    """
+    Concrete implementation of a StateMachineTester for a pytransitions
+    state machine (with autotransitions turned on). The states and
+    actions in the state machine specification must correspond exactly
+    with the machine's states and triggers.
+    """
+
+    def assert_state(self, machine_under_test, state):
+        """
+        Assert the current state of the state machine under test.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param state: the state that we are asserting to be the current
+            state of the state machine under test
+        :type state: str
+        """
+        assert machine_under_test.state == state
+
+    def is_action_allowed(self, machine_under_test, action):
+        """
+        Check whether the action under test is allowed from the current
+        state.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        return action in machine_under_test.get_triggers(machine_under_test.state)
+
+    def perform_action(self, machine_under_test, action):
+        """
+        Perform a given action on the state machine under test.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        machine_under_test.trigger(action)
+
+    def check_action_fails(self, machine_under_test, action):
+        """
+        Check that attempting a given action on the state machine under
+        test fails in its current state.
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        with pytest.raises(MachineError):
+            self.perform_action(machine_under_test, action)
+
+    def to_state(self, machine_under_test, target_state):
+        """
+        Transition the state machine to a target state. This
+        implementation uses autotransitions. If the pytransitions state
+        machine under test has autotransitions turned off, then this
+        method will need to be overridden by some other method of
+        putting the machine into the state under test.
+
+        :param machine: the state machine under test
+        :type machine: state machine object instance
+        :param target_state: the state that we want to get the state
+            machine under test into
+        :type target_state: str
+        """
+        machine_under_test.trigger(f"to_{target_state}")
+
+
+class StateModelTester(StateMachineTester):
+    """
+    Abstract base class for testing state models using state machines.
+
+    The ``assert_state`` method has to be implemented in concrete classes,
+    and the `machine_under_test` fixture must also be provided.
+    """
+
+    def assert_state(self, machine_under_test, state):
+        """
+        Assert the current state of the state model under test.
+
+        :param machine_under_test: the state model under test
+        :type machine_under_test: object
+        :param state: the state that we are asserting to be the current
+            state of the state machine under test
+        :type state: dict
+        """
+        raise NotImplementedError()
+
+    def is_action_allowed(self, machine_under_test, action):
+        """
+        Returns whether the state machine under test thinks an action
+        is permitted in its current state
+
+        :param machine_under_test: the state model under test
+        :type machine_under_test: object
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        return machine_under_test.is_action_allowed(action)
+
+    def perform_action(self, machine_under_test, action):
+        """
+        Perform a given action on the state machine under test.
+
+        :param machine_under_test: the state model under test
+        :type machine_under_test: object
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        machine_under_test.perform_action(action)
+
+    def check_action_fails(self, machine_under_test, action):
+        """
+        Assert that performing a given action on the state maching under
+        test fails in its current state.
+
+        :param machine_under_test: the state model under test
+        :type machine_under_test: object
+        :param action: action to be performed on the state machine
+        :type action: str
+        """
+        with pytest.raises(StateModelError):
+            self.perform_action(machine_under_test, action)
+
+    def to_state(self, machine_under_test, target_state):
+        """
+        Transition the state machine to a target state.
+
+        :param machine_under_test: the state model under test
+        :type machine_under_test: object
+        :param target_state: specification of the target state that we
+            want to get the state machine under test into
+        :type target_state: keyword dictionary
+        """
+        machine_under_test._straight_to_state(**target_state)
+
+
+def load_data(name):
+    """
+    Loads a dataset by name. This implementation uses the name to find a
+    JSON file containing the data to be loaded.
+
+    :param name: name of the dataset to be loaded; this implementation
+        uses the name to find a JSON file containing the data to be
+        loaded.
+    :type name: string
+    """
+    with open(f"tests/state/data/{name}.json", "r") as json_file:
+        return json.load(json_file)
+
+
+def load_state_machine_spec(name):
+    """
+    Loads a state machine specification by name.
+
+    :param name: name of the dataset to be loaded; this implementation
+        uses the name to find a JSON file containing the data to be
+        loaded.
+    :type name: string
+    """
+    machine_spec = load_data(name)
+    for state in machine_spec["states"]:
+        state_spec = machine_spec["states"][state]
+        if "admin_mode" in state_spec:
+            state_spec["admin_mode"] = AdminMode[state_spec["admin_mode"]]
+        if "op_state" in state_spec:
+            state_spec["op_state"] = getattr(DevState, state_spec["op_state"])
+        if "obs_state" in state_spec:
+            state_spec["obs_state"] = ObsState[state_spec["obs_state"]]
+    return machine_spec
diff --git a/tests/data/admin_mode_state_machine.json b/tests/state/data/admin_mode_model.json
similarity index 100%
rename from tests/data/admin_mode_state_machine.json
rename to tests/state/data/admin_mode_model.json
diff --git a/tests/state/data/csp_subelement_obs_state_machine.json b/tests/state/data/csp_subelement_obs_state_machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..f03bcb3807464a51a55c80cac72decbda1643634
--- /dev/null
+++ b/tests/state/data/csp_subelement_obs_state_machine.json
@@ -0,0 +1,190 @@
+{
+    "states": {
+        "IDLE": {},
+        "CONFIGURING_IDLE": {},
+        "CONFIGURING_READY": {},
+        "READY": {},
+        "SCANNING": {},
+        "ABORTING": {},
+        "ABORTED": {},
+        "RESETTING": {},
+        "FAULT": {}
+    },
+    "transitions": [
+        {
+            "from": "IDLE",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "READY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "SCANNING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "ABORTING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "ABORTED",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "FAULT",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "RESETTING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "IDLE",
+            "to": "CONFIGURING_IDLE",
+            "trigger": "configure_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "IDLE",
+            "trigger": "configure_completed"
+        },
+        {
+            "from": "READY",
+            "to": "CONFIGURING_READY",
+            "trigger": "configure_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "CONFIGURING_READY",
+            "trigger": "component_configured"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "READY",
+            "trigger": "configure_completed"
+        },
+        {
+            "from": "READY",
+            "to": "READY",
+            "trigger": "end_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "IDLE",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "READY",
+            "to": "READY",
+            "trigger": "scan_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "SCANNING",
+            "trigger": "component_scanning"
+        },
+        {
+            "from": "SCANNING",
+            "to": "SCANNING",
+            "trigger": "end_scan_invoked"
+        },
+        {
+            "from": "SCANNING",
+            "to": "READY",
+            "trigger": "component_not_scanning"
+        },
+        {
+            "from": "IDLE",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "SCANNING",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "RESETTING",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_configured"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_not_scanning"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_scanning"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTED",
+            "trigger": "abort_completed"
+        },
+        {
+            "from": "ABORTED",
+            "to": "RESETTING",
+            "trigger": "obsreset_invoked"
+        },
+        {
+            "from": "FAULT",
+            "to": "RESETTING",
+            "trigger": "obsreset_invoked"
+        },
+        {
+            "from": "RESETTING",
+            "to": "RESETTING",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "RESETTING",
+            "to": "IDLE",
+            "trigger": "obsreset_completed"
+        }
+    ]
+}
diff --git a/tests/state/data/op_state_machine.json b/tests/state/data/op_state_machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..ab5fe1c10a5d2b0df7235bd5525256639d9346ad
--- /dev/null
+++ b/tests/state/data/op_state_machine.json
@@ -0,0 +1,464 @@
+{
+    "states": {
+        "_UNINITIALISED": {},
+        "INIT_UNKNOWN": {},
+        "INIT_DISABLE": {},
+        "INIT_OFF": {},
+        "INIT_STANDBY": {},
+        "INIT_ON": {},
+        "INIT_FAULT": {},
+        "UNKNOWN": {},
+        "DISABLE": {},
+        "OFF": {},
+        "STANDBY": {},
+        "ON": {},
+        "FAULT": {}
+    },
+    "transitions": [
+        {
+            "from": "_UNINITIALISED",
+            "to": "INIT_DISABLE",
+            "trigger": "init_invoked"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "INIT_FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "INIT_UNKNOWN",
+            "to": "UNKNOWN",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "INIT_DISABLE",
+            "to": "DISABLE",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "INIT_OFF",
+            "to": "OFF",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "INIT_STANDBY",
+            "to": "STANDBY",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "INIT_ON",
+            "to": "ON",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "INIT_FAULT",
+            "to": "FAULT",
+            "trigger": "init_completed"
+        },
+        {
+            "from": "DISABLE",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "DISABLE",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "DISABLE",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "DISABLE",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "DISABLE",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "DISABLE",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "UNKNOWN",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "OFF",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "OFF",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "OFF",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "OFF",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "OFF",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "OFF",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "STANDBY",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "STANDBY",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "STANDBY",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "STANDBY",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "STANDBY",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "STANDBY",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "ON",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "ON",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "ON",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "ON",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "ON",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "ON",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "FAULT",
+            "to": "DISABLE",
+            "trigger": "component_disconnected"
+        },
+        {
+            "from": "FAULT",
+            "to": "UNKNOWN",
+            "trigger": "component_unknown"
+        },
+        {
+            "from": "FAULT",
+            "to": "OFF",
+            "trigger": "component_off"
+        },
+        {
+            "from": "FAULT",
+            "to": "STANDBY",
+            "trigger": "component_standby"
+        },
+        {
+            "from": "FAULT",
+            "to": "ON",
+            "trigger": "component_on"
+        },
+        {
+            "from": "FAULT",
+            "to": "FAULT",
+            "trigger": "component_fault"
+        },
+        {
+            "from": "OFF",
+            "to": "OFF",
+            "trigger": "off_invoked"
+        },
+        {
+            "from": "STANDBY",
+            "to": "STANDBY",
+            "trigger": "off_invoked"
+        },
+        {
+            "from": "ON",
+            "to": "ON",
+            "trigger": "off_invoked"
+        },
+        {
+            "from": "OFF",
+            "to": "OFF",
+            "trigger": "standby_invoked"
+        },
+        {
+            "from": "STANDBY",
+            "to": "STANDBY",
+            "trigger": "standby_invoked"
+        },
+        {
+            "from": "ON",
+            "to": "ON",
+            "trigger": "standby_invoked"
+        },
+        {
+            "from": "OFF",
+            "to": "OFF",
+            "trigger": "on_invoked"
+        },
+        {
+            "from": "STANDBY",
+            "to": "STANDBY",
+            "trigger": "on_invoked"
+        },
+        {
+            "from": "ON",
+            "to": "ON",
+            "trigger": "on_invoked"
+        },
+        {
+            "from": "FAULT",
+            "to": "FAULT",
+            "trigger": "reset_invoked"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/state/data/subarray_state_machine.json b/tests/state/data/subarray_state_machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e8fed8a5d9c43443fae478bc83da9f7a9ef73bb
--- /dev/null
+++ b/tests/state/data/subarray_state_machine.json
@@ -0,0 +1,289 @@
+{
+    "states": {
+        "EMPTY": {},
+        "RESOURCING_EMPTY": {},
+        "RESOURCING_IDLE": {},
+        "IDLE": {},
+        "CONFIGURING_IDLE": {},
+        "CONFIGURING_READY": {},
+        "READY": {},
+        "SCANNING": {},
+        "ABORTING": {},
+        "ABORTED": {},
+        "RESETTING": {},
+        "RESTARTING": {},
+        "FAULT": {}
+    },
+    "transitions": [
+        {
+            "from": "EMPTY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "RESOURCING_EMPTY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "RESOURCING_IDLE",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "IDLE",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "READY",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "SCANNING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "ABORTING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "ABORTED",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "FAULT",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "RESETTING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "RESTARTING",
+            "to": "FAULT",
+            "trigger": "component_obsfault"
+        },
+        {
+            "from": "EMPTY",
+            "to": "RESOURCING_EMPTY",
+            "trigger": "assign_invoked"
+        },
+        {
+            "from": "EMPTY",
+            "to": "RESOURCING_EMPTY",
+            "trigger": "release_invoked"
+        },
+        {
+            "from": "IDLE",
+            "to": "RESOURCING_IDLE",
+            "trigger": "assign_invoked"
+        },
+        {
+            "from": "IDLE",
+            "to": "RESOURCING_IDLE",
+            "trigger": "release_invoked"
+        },
+        {
+            "from": "RESOURCING_EMPTY",
+            "to": "RESOURCING_IDLE",
+            "trigger": "component_resourced"
+        },
+        {
+            "from": "RESOURCING_IDLE",
+            "to": "RESOURCING_EMPTY",
+            "trigger": "component_unresourced"
+        },
+        {
+            "from": "RESOURCING_EMPTY",
+            "to": "EMPTY",
+            "trigger": "assign_completed"
+        },
+        {
+            "from": "RESOURCING_EMPTY",
+            "to": "EMPTY",
+            "trigger": "release_completed"
+        },
+        {
+            "from": "RESOURCING_IDLE",
+            "to": "IDLE",
+            "trigger": "assign_completed"
+        },
+        {
+            "from": "RESOURCING_IDLE",
+            "to": "IDLE",
+            "trigger": "release_completed"
+        },
+        {
+            "from": "IDLE",
+            "to": "CONFIGURING_IDLE",
+            "trigger": "configure_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "IDLE",
+            "trigger": "configure_completed"
+        },
+        {
+            "from": "READY",
+            "to": "CONFIGURING_READY",
+            "trigger": "configure_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "CONFIGURING_READY",
+            "trigger": "component_configured"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "READY",
+            "trigger": "configure_completed"
+        },
+        {
+            "from": "READY",
+            "to": "READY",
+            "trigger": "end_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "IDLE",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "READY",
+            "to": "READY",
+            "trigger": "scan_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "SCANNING",
+            "trigger": "component_scanning"
+        },
+        {
+            "from": "SCANNING",
+            "to": "SCANNING",
+            "trigger": "end_scan_invoked"
+        },
+        {
+            "from": "SCANNING",
+            "to": "READY",
+            "trigger": "component_not_scanning"
+        },
+        {
+            "from": "IDLE",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "CONFIGURING_IDLE",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "CONFIGURING_READY",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "READY",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "SCANNING",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "RESETTING",
+            "to": "ABORTING",
+            "trigger": "abort_invoked"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_configured"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_not_scanning"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTING",
+            "trigger": "component_scanning"
+        },
+        {
+            "from": "ABORTING",
+            "to": "ABORTED",
+            "trigger": "abort_completed"
+        },
+        {
+            "from": "ABORTED",
+            "to": "RESETTING",
+            "trigger": "obsreset_invoked"
+        },
+        {
+            "from": "FAULT",
+            "to": "RESETTING",
+            "trigger": "obsreset_invoked"
+        },
+        {
+            "from": "RESETTING",
+            "to": "RESETTING",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "RESETTING",
+            "to": "IDLE",
+            "trigger": "obsreset_completed"
+        },
+        {
+            "from": "ABORTED",
+            "to": "RESTARTING",
+            "trigger": "restart_invoked"
+        },
+        {
+            "from": "FAULT",
+            "to": "RESTARTING",
+            "trigger": "restart_invoked"
+        },
+        {
+            "from": "RESTARTING",
+            "to": "RESTARTING",
+            "trigger": "component_unconfigured"
+        },
+        {
+            "from": "RESTARTING",
+            "to": "RESTARTING",
+            "trigger": "component_unresourced"
+        },
+        {
+            "from": "RESTARTING",
+            "to": "EMPTY",
+            "trigger": "restart_completed"
+        }
+    ]
+}
diff --git a/tests/state/test_admin_mode_model.py b/tests/state/test_admin_mode_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..4843f5a1ae417e30aba74db76f8cdd3f97e73709
--- /dev/null
+++ b/tests/state/test_admin_mode_model.py
@@ -0,0 +1,45 @@
+"""
+This module contains the tests for the
+:py:mod:`ska_tango_base.base.admin_mode_model` module.
+"""
+import pytest
+
+from ska_tango_base.control_model import AdminMode
+from ska_tango_base.base import AdminModeModel
+
+from .conftest import load_state_machine_spec, StateModelTester
+
+
+@pytest.mark.state_machine_tester(load_state_machine_spec("admin_mode_model"))
+class TestAdminModeModel(StateModelTester):
+    """
+    This class contains the test for the AdminModeModel class.
+    """
+
+    @pytest.fixture
+    def machine_under_test(self, logger):
+        """
+        Fixture that returns the state model under test in this class
+
+        :param logger: a logger for the model under test
+        :type logger: :py:class:`logging.Logger`
+
+        :returns: the state model under test
+        :rtype: :py:class:`ska_tango_base.base.AdminModeModel`
+        """
+        return AdminModeModel(logger)
+
+    def assert_state(self, machine_under_test, state):
+        """
+        Assert the current admin mode of the model under test
+
+        :param machine_under_test: the state machine under test
+        :type machine_under_test: state machine object instance
+        :param state: the state that we are asserting to be the current
+            state of the state machine under test
+        :type state: dict
+        """
+        assert machine_under_test.admin_mode == AdminMode[state]
+
+    def to_state(self, machine_under_test, state):
+        machine_under_test._straight_to_state(admin_mode=AdminMode[state])
diff --git a/tests/state/test_csp_subelement_obs_state_model.py b/tests/state/test_csp_subelement_obs_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..24ca7c0c8dadaf2261ce722bf70681bd0ca312ff
--- /dev/null
+++ b/tests/state/test_csp_subelement_obs_state_model.py
@@ -0,0 +1,29 @@
+"""
+Module to test the transitions of a CSP SubElement ObsDevice
+(ska_tango_base.csp_subelement_obsdev_state_machine module).
+"""
+import pytest
+
+from ska_tango_base.csp.obs.obs_state_model import (
+    _CspSubElementObsStateMachine,
+)
+
+from .conftest import load_state_machine_spec, TransitionsStateMachineTester
+
+
+@pytest.mark.state_machine_tester(
+    load_state_machine_spec("csp_subelement_obs_state_machine")
+)
+class TestCspSubElementObsStateMachine(TransitionsStateMachineTester):
+    """
+    This class contains the test for the CspSubElementObsStateMachine class.
+    """
+
+    @pytest.fixture
+    def machine_under_test(self):
+        """
+        Fixture that returns the state model under test in this class
+
+        :returns: the state machine under test
+        """
+        yield _CspSubElementObsStateMachine()
diff --git a/tests/state/test_op_state_model.py b/tests/state/test_op_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..567cadcc4e87dd3ab5f7c52dd6557099adf1499b
--- /dev/null
+++ b/tests/state/test_op_state_model.py
@@ -0,0 +1,25 @@
+"""
+This module contains the tests for the
+:py:mod:`ska_tango_base.base.device_state_model` module.
+"""
+import pytest
+
+from ska_tango_base.base.op_state_model import _OpStateMachine
+
+from .conftest import load_state_machine_spec, TransitionsStateMachineTester
+
+
+@pytest.mark.state_machine_tester(load_state_machine_spec("op_state_machine"))
+class TestOpStateMachine(TransitionsStateMachineTester):
+    """
+    This class contains the test for the _OpStateMachine class.
+    """
+
+    @pytest.fixture
+    def machine_under_test(self):
+        """
+        Fixture that returns the state machine under test in this class
+
+        :returns: the state machine under test
+        """
+        yield _OpStateMachine()
diff --git a/tests/state/test_subarray_obs_state_model.py b/tests/state/test_subarray_obs_state_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e08fb58d2704afceb5e33733e65014a21cc2c55
--- /dev/null
+++ b/tests/state/test_subarray_obs_state_model.py
@@ -0,0 +1,25 @@
+"""
+This module contains the tests for the
+:py:mod:`ska_tango_base.subarray.subarray_state_model` module.
+"""
+import pytest
+
+from ska_tango_base.subarray.subarray_obs_state_model import _SubarrayObsStateMachine
+
+from .conftest import load_state_machine_spec, TransitionsStateMachineTester
+
+
+@pytest.mark.state_machine_tester(load_state_machine_spec("subarray_state_machine"))
+class TestSubarrayObsStateMachine(TransitionsStateMachineTester):
+    """
+    This class contains the test for the SubarrayObsStateModel class.
+    """
+
+    @pytest.fixture
+    def machine_under_test(self):
+        """
+        Fixture that returns the state model under test in this class
+
+        :returns: the state machine under test
+        """
+        yield _SubarrayObsStateMachine()
diff --git a/tests/test_alarm_handler_device.py b/tests/test_alarm_handler_device.py
index 3757d8b512f632be1ab005176fb98eb73abbba34..53d14943dce3112528a51c35516ac4a6fbce40e5 100644
--- a/tests/test_alarm_handler_device.py
+++ b/tests/test_alarm_handler_device.py
@@ -10,8 +10,12 @@
 
 # Imports
 import re
+
 import pytest
 
+from ska_tango_base import SKAAlarmHandler
+from ska_tango_base.base import ReferenceBaseComponentManager
+from ska_tango_base.control_model import AdminMode
 
 # PROTECTED REGION ID(SKAAlarmHandler.test_additional_imports) ENABLED START #
 # PROTECTED REGION END #    //  SKAAlarmHandler.test_additional_imports
@@ -20,24 +24,26 @@ import pytest
 @pytest.mark.usefixtures("tango_context", "initialize_device")
 # PROTECTED REGION END #    //  SKAAlarmHandler.test_SKAAlarmHandler_decorators
 class TestSKAAlarmHandler(object):
-    """Test case for packet generation."""
-
-    properties = {
-        'SubAlarmHandlers': '',
-        'AlarmConfigFile': '',
-        'SkaLevel': '4',
-        'GroupDefinitions': '',
-        'LoggingTargetsDefault': ''
+    """
+    Test class for tests of the SKAAlarmHander device class.
+    """
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+        """
+        return {
+            "device": SKAAlarmHandler,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
         }
 
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKAAlarmHandler.numpy = MagicMock()
-        # PROTECTED REGION ID(SKAAlarmHandler.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKAAlarmHandler.test_mocking
-
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         """Test the device properties"""
         # Test the properties
@@ -90,8 +96,9 @@ class TestSKAAlarmHandler(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKAAlarmHandler.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKAAlarmHandler, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKAAlarmHandler.test_GetVersionInfo
diff --git a/tests/test_base_component_manager.py b/tests/test_base_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..4fc2a2ae81a9f54748e6a12161abffc02890a645
--- /dev/null
+++ b/tests/test_base_component_manager.py
@@ -0,0 +1,241 @@
+"""
+Tests for the :py:mod:`ska_tango_base.component_manager` module.
+"""
+import contextlib
+import pytest
+
+from ska_tango_base.base import ReferenceBaseComponentManager
+from ska_tango_base.control_model import PowerMode
+from ska_tango_base.faults import ComponentFault
+
+
+class TestBaseComponentManager:
+    """
+    Tests of the
+    :py:class:`ska_tango_base.component_manager.ComponentManager`
+    class.
+    """
+
+    @pytest.fixture()
+    def mock_op_state_model(self, mocker):
+        """
+        Fixture that returns a mock state model
+
+        :param mocker: pytest fixture that wraps
+            :py:mod:`unittest.mock`.
+
+        :return: a mock state model
+        """
+        return mocker.Mock()
+
+    @pytest.fixture(params=[PowerMode.OFF, PowerMode.STANDBY, PowerMode.ON])
+    def initial_power_mode(self, request):
+        return request.param
+
+    @pytest.fixture(params=[False, True])
+    def initial_fault(self, request):
+        return request.param
+
+    @pytest.fixture()
+    def component(self, initial_power_mode, initial_fault):
+        return ReferenceBaseComponentManager._Component(
+            _power_mode=initial_power_mode, _faulty=initial_fault
+        )
+
+    @pytest.fixture()
+    def component_manager(self, mock_op_state_model, logger, component):
+        """
+        Fixture that returns the component manager under test
+
+        :param mock_op_state_model: a mock state model for testing
+        :param logger: a logger for the component manager
+
+        :return: the component manager under test
+        """
+        return ReferenceBaseComponentManager(mock_op_state_model, logger=logger, _component=component)
+
+    def test_state_changes_with_start_and_stop(
+        self, component_manager, mock_op_state_model, initial_power_mode, initial_fault
+    ):
+        """
+        Test that the state model is updated with state changes when the
+        component manager connects to and disconnects from its
+        component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+        expected_action = (
+            "component_fault" if initial_fault else power_mode_map[initial_power_mode]
+        )
+
+        assert not component_manager.is_communicating
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+        assert mock_op_state_model.perform_action.call_args_list == [
+            (("component_unknown",),),
+            ((expected_action,),),
+        ]
+
+        mock_op_state_model.reset_mock()
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with(
+            "component_disconnected"
+        )
+
+    def test_simulate_communication_failure(self, component_manager, mock_op_state_model):
+        """
+        Test that we can simulate connection failure.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        mock_op_state_model.reset_mock()
+        component_manager.simulate_communication_failure(True)
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with("component_unknown")
+
+        with pytest.raises(ConnectionError, match="Failed to connect"):
+            component_manager.start_communicating()
+
+    @pytest.mark.parametrize("command", ["off", "standby", "on"])
+    def test_command_fails_when_disconnected(self, component_manager, command):
+        """
+        Test that component commands fail when the component manager
+        isn't connected to the component.
+
+        :param component_manager: the component manager under test
+        :param command: the command under test
+        """
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+    @pytest.mark.parametrize(
+        ("command", "action"),
+        [
+            ("off", "component_off"),
+            ("standby", "component_standby"),
+            ("on", "component_on"),
+        ],
+    )
+    def test_command_succeeds_when_connected(
+        self,
+        component_manager,
+        mock_op_state_model,
+        initial_power_mode,
+        initial_fault,
+        command,
+        action,
+    ):
+        """
+        Test that component commands succeed when the component manager
+        is connected to the component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param command: the name of the command under test
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component_manager, command)()
+
+            if power_mode_map[initial_power_mode] == action:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    @pytest.mark.parametrize(
+        ("call", "action"),
+        [
+            ("simulate_off", "component_off"),
+            ("simulate_standby", "component_standby"),
+            ("simulate_on", "component_on"),
+        ],
+    )
+    def test_simulated_change_propagates(
+        self,
+        component_manager,
+        mock_op_state_model,
+        component,
+        initial_power_mode,
+        initial_fault,
+        call,
+        action,
+    ):
+        """
+        Test that spontaneous changes to the state of the component
+        result in the correct action being performed on the state model.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param state:
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component, call)()
+            if action == power_mode_map[initial_power_mode]:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    def test_reset_from_fault(
+        self, component_manager, mock_op_state_model, initial_fault
+    ):
+        component_manager.start_communicating()
+        assert component_manager.faulty == initial_fault
+        mock_op_state_model.reset_mock()
+
+        component_manager.reset()
+        assert not component_manager.faulty
+        mock_op_state_model.perform_action.assert_not_called()
diff --git a/tests/test_base_device.py b/tests/test_base_device.py
index 5e79d03e92440e88dd63e2d0de5505a44f17bd0b..ef16b9f2ce59cc2b82c25aaf8a2f2fb2525eeafa 100644
--- a/tests/test_base_device.py
+++ b/tests/test_base_device.py
@@ -20,23 +20,22 @@ import tango
 from unittest import mock
 from tango import DevFailed, DevState
 from ska_tango_base import SKABaseDevice
-from ska_tango_base.commands import ResultCode
-from ska_tango_base.control_model import (
-    AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode
-)
-from ska_tango_base.base_device import (
+from ska_tango_base.base import OpStateModel, ReferenceBaseComponentManager
+from ska_tango_base.base.base_device import (
     _DEBUGGER_PORT,
     _Log4TangoLoggingLevel,
     _PYTHON_TO_TANGO_LOGGING_LEVEL,
     LoggingUtils,
     LoggingTargetError,
-    DeviceStateModel,
     TangoLoggingServiceHandler,
 )
+from ska_tango_base.commands import ResultCode
+from ska_tango_base.control_model import (
+    AdminMode, ControlMode, HealthState, LoggingLevel, SimulationMode, TestMode
+)
 from ska_tango_base.faults import CommandError
 
-from .conftest import load_state_machine_spec, ModelStateMachineTester
-
+from .state.conftest import load_state_machine_spec
 # PROTECTED REGION END #    //  SKABaseDevice.test_additional_imports
 # Device test case
 # PROTECTED REGION ID(SKABaseDevice.test_SKABaseDevice_decorators) ENABLED START #
@@ -205,7 +204,7 @@ class TestLoggingUtils:
         with pytest.raises(LoggingTargetError):
             LoggingUtils.get_syslog_address_and_socktype(bad_syslog_url)
 
-    @mock.patch('ska_tango_base.base_device.TangoLoggingServiceHandler')
+    @mock.patch('ska_tango_base.base.base_device.TangoLoggingServiceHandler')
     @mock.patch('logging.handlers.SysLogHandler')
     @mock.patch('logging.handlers.RotatingFileHandler')
     @mock.patch('logging.StreamHandler')
@@ -329,67 +328,32 @@ class TestLoggingUtils:
             LoggingUtils.create_logging_handler = orig_create_logging_handler
 
 
-@pytest.fixture
-def device_state_model():
-    """
-    Yields a new DeviceStateModel for testing
-
-    :yields: a DeviceStateModel instance to be tested
-    """
-    yield DeviceStateModel(logging.getLogger())
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("device_state_machine"))
-class TestDeviceStateModel(ModelStateMachineTester):
-    """
-    This class contains the test suite for the ska_tango_base.SKABaseDevice class.
-    """
-
-    @pytest.fixture
-    def machine(self, device_state_model):
-        """
-        Fixture that returns the state machine under test in this class
-
-        :yields: the state machine under test
-        """
-        yield device_state_model
-
-    def assert_state(self, machine, state):
-        """
-        Assert the current state of this state machine, based on the
-        values of the adminMode, opState and obsState attributes of this
-        model.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: dict
-        """
-        assert machine.admin_mode == state["admin_mode"]
-        assert machine.op_state == state["op_state"]
-
-
 # PROTECTED REGION END #    //  SKABaseDevice.test_SKABaseDevice_decorators
 class TestSKABaseDevice(object):
     """
     Test cases for SKABaseDevice.
     """
 
-    properties = {
-        'SkaLevel': '4',
-        'GroupDefinitions': '',
-        'LoggingTargetsDefault': ''
-    }
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+
+        This implementation provides a concrete subclass of
+        SKABaseDevice, and a memorized value for adminMode.
+        """
+        return {
+            "device": SKABaseDevice,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
 
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKABaseDevice.numpy = MagicMock()
-        # PROTECTED REGION ID(SKABaseDevice.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKABaseDevice.test_mocking
 
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # Test the properties
         # PROTECTED REGION ID(SKABaseDevice.test_properties) ENABLED START #
@@ -426,8 +390,9 @@ class TestSKABaseDevice(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKABaseDevice.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKABaseDevice, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKABaseDevice.test_GetVersionInfo
@@ -447,7 +412,6 @@ class TestSKABaseDevice(object):
         """
         Test for On command
         """
-
         state_callback = tango_change_event_helper.subscribe("state")
         status_callback = tango_change_event_helper.subscribe("status")
         state_callback.assert_call(DevState.OFF)
@@ -463,25 +427,10 @@ class TestSKABaseDevice(object):
         state_callback.assert_not_called()
         status_callback.assert_not_called()
 
-    def test_Disable(self, tango_context):
-        """
-        Test for Disable command
-        """
-        assert tango_context.device.state() == DevState.OFF
-
-        # Check that we can disable it
-        tango_context.device.Disable()
-        assert tango_context.device.state() == DevState.DISABLE
-
-        # Check that we can disable it when it is already disabled
-        tango_context.device.Disable()
-
     def test_Standby(self, tango_context):
         """
         Test for Standby command
         """
-        assert tango_context.device.state() == DevState.OFF
-
         # Check that we can put it on standby
         tango_context.device.Standby()
         assert tango_context.device.state() == DevState.STANDBY
@@ -548,7 +497,7 @@ class TestSKABaseDevice(object):
         assert tango_context.device.loggingTargets == ("tango::logger", )
 
         with mock.patch(
-            "ska_tango_base.base_device.LoggingUtils.create_logging_handler"
+            "ska_tango_base.base.base_device.LoggingUtils.create_logging_handler"
         ) as mocked_creator:
 
             def null_creator(target, tango_logger):
@@ -606,24 +555,26 @@ class TestSKABaseDevice(object):
     def test_adminMode(self, tango_context, tango_change_event_helper):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKABaseDevice.test_adminMode) ENABLED START #
-        tango_context.device.Disable()
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
+        assert tango_context.device.state() == DevState.OFF
 
         admin_mode_callback = tango_change_event_helper.subscribe("adminMode")
-        admin_mode_callback.assert_call(AdminMode.MAINTENANCE)
+        admin_mode_callback.assert_call(AdminMode.ONLINE)
 
         tango_context.device.adminMode = AdminMode.OFFLINE
         assert tango_context.device.adminMode == AdminMode.OFFLINE
         admin_mode_callback.assert_call(AdminMode.OFFLINE)
+        assert tango_context.device.state() == DevState.DISABLE
 
-        tango_context.device.adminMode = AdminMode.ONLINE
-        assert tango_context.device.adminMode == AdminMode.ONLINE
-        admin_mode_callback.assert_call(AdminMode.ONLINE)
+        tango_context.device.adminMode = AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        admin_mode_callback.assert_call(AdminMode.MAINTENANCE)
+        assert tango_context.device.state() == DevState.OFF
 
         tango_context.device.adminMode = AdminMode.ONLINE
         assert tango_context.device.adminMode == AdminMode.ONLINE
-        admin_mode_callback.assert_not_called()
-
+        admin_mode_callback.assert_call(AdminMode.ONLINE)
+        assert tango_context.device.state() == DevState.OFF
         # PROTECTED REGION END #    //  SKABaseDevice.test_adminMode
 
     # PROTECTED REGION ID(SKABaseDevice.test_controlMode_decorators) ENABLED START #
@@ -678,44 +629,99 @@ class TestSKABaseDevice(object):
 
 class TestSKABaseDevice_commands:
     """
-    This class contains tests of SKASubarray commands
+    This class contains tests of SKABaseDevice commands
     """
+    @pytest.fixture
+    def op_state_model(self, logger):
+        """
+        Yields a new OpStateModel for testing
 
-    def test_ResetCommand(self, device_state_model):
+        :yields: a OpStateModel instance to be tested
         """
-        Test for SKBaseDevice.ResetCommand
+        yield OpStateModel(logger)
+
+    @pytest.fixture
+    def command_factory(self, mocker, op_state_model):
         """
-        mock_device = mock.Mock()
+        Returns a factory that constructs a command object for a given
+        class
 
-        reset_command = SKABaseDevice.ResetCommand(
-            mock_device,
-            device_state_model
-        )
+        :returns: a factory that constructs a command object for a given
+        class
+        """
+        def _command_factory(command):
+            return command(mocker.Mock(), op_state_model)
 
-        machine_spec = load_state_machine_spec("device_state_machine")
-        states = machine_spec["states"]
+        return _command_factory
+
+    @pytest.fixture
+    def machine_spec(self):
+        return load_state_machine_spec("op_state_machine")
 
-        transitions = {
-            "FAULT_MAINTENANCE": "OFF_MAINTENANCE",
-            "FAULT_ONLINE": "OFF_ONLINE",
+    @pytest.fixture()
+    def op_state_mapping(self):
+        return {
+            "_UNINITIALISED": None,
+            "INIT_DISABLE": DevState.INIT,
+            "INIT_UNKNOWN": DevState.INIT,
+            "INIT_OFF": DevState.INIT,
+            "INIT_STANDBY": DevState.INIT,
+            "INIT_ON": DevState.INIT,
+            "INIT_FAULT": DevState.INIT,
+            "DISABLE": DevState.DISABLE,
+            "UNKNOWN": DevState.UNKNOWN,
+            "OFF": DevState.OFF,
+            "STANDBY": DevState.STANDBY,
+            "ON": DevState.ON,
+            "FAULT": DevState.FAULT,
         }
 
+    @pytest.mark.parametrize(
+        ("command_class", "slug"),
+        [
+            (SKABaseDevice.ResetCommand, "reset"),
+            (SKABaseDevice.OffCommand, "off"),
+            (SKABaseDevice.StandbyCommand, "standby"),
+            (SKABaseDevice.OnCommand, "on"),
+        ]
+    )
+    def test_Command(
+        self,
+        machine_spec,
+        op_state_mapping,
+        command_factory,
+        op_state_model,
+        command_class,
+        slug
+    ):
+        """
+        Test that certain commands can only be invoked in certain
+        states, and that the result of invoking the command is as
+        expected.
+        """
+        command = command_factory(command_class)
+
+        states = machine_spec["states"]
+        transitions = machine_spec["transitions"]
+        transitions = [t for t in transitions if t.get("trigger") == f"{slug}_invoked"]
+
         for state in states:
-            device_state_model._straight_to_state(**states[state])
-            if state in transitions:
-                # the reset command is permitted, should succeed.
-                assert reset_command.is_allowed()
-                assert reset_command() == (
-                    ResultCode.OK,
-                    "Reset command completed OK",
-                )
-                expected = transitions[state]
+            op_state_model._straight_to_state(state)
+
+            transitions_from_state = [t for t in transitions if t.get("from") == state]
+
+            if transitions_from_state:
+                # this command is permitted in the current state, should succeed,
+                # should result in the correct transition.
+                assert command.is_allowed()
+                (result_code, _) = command()
+                assert result_code == ResultCode.OK
+                expected = transitions_from_state[0]["to"]
             else:
-                # the reset command is not permitted, should not be allowed,
-                # should fail, should have no side-effect
-                assert not reset_command.is_allowed()
+                # this command is not permitted, should not be allowed,
+                # should fail, should have no side-effect.
+                assert not command.is_allowed()
                 with pytest.raises(CommandError):
-                    reset_command()
+                    command()
                 expected = state
-            assert device_state_model.admin_mode == states[expected]["admin_mode"]
-            assert device_state_model.op_state == states[expected]["op_state"]
+            assert op_state_model.op_state == op_state_mapping[expected]
diff --git a/tests/test_capability_device.py b/tests/test_capability_device.py
index fdc287abcf734f3446f9121815054a933c47cdee..945c8d20d23593379d3242a1ef6b5ede6a3daf53 100644
--- a/tests/test_capability_device.py
+++ b/tests/test_capability_device.py
@@ -10,6 +10,10 @@
 import re
 import pytest
 
+from ska_tango_base import SKACapability
+from ska_tango_base.base import ReferenceBaseComponentManager
+from ska_tango_base.control_model import AdminMode
+
 
 # PROTECTED REGION ID(SKACapability.test_additional_imports) ENABLED START #
 # PROTECTED REGION END #    //  SKACapability.test_additional_imports
@@ -20,23 +24,22 @@ import pytest
 class TestSKACapability(object):
     """Test case for packet generation."""
 
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-        'CapType': '',
-        'CapID': '',
-        'subID': '',
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+        """
+        return {
+            "device": SKACapability,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
         }
 
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKACapability.numpy = MagicMock()
-        # PROTECTED REGION ID(SKACapability.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKACapability.test_mocking
-
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         """Test device properties"""
         # Test the properties
diff --git a/tests/test_csp_subelement_master.py b/tests/test_csp_master.py
similarity index 89%
rename from tests/test_csp_subelement_master.py
rename to tests/test_csp_master.py
index 694fb89360629784d7a769e189c3cd9eec07281a..e911d6d9779e57e1c60d3eea0fd46b2b5e734613 100644
--- a/tests/test_csp_subelement_master.py
+++ b/tests/test_csp_master.py
@@ -17,8 +17,8 @@ from tango.test_context import MultiDeviceTestContext
 
 # PROTECTED REGION ID(CspSubelementMaster.test_additional_imports) ENABLED START #
 from ska_tango_base import SKAMaster, CspSubElementMaster
+from ska_tango_base.base import ReferenceBaseComponentManager
 from ska_tango_base.commands import ResultCode
-from ska_tango_base.faults import CommandError
 from ska_tango_base.control_model import (
     AdminMode, ControlMode, HealthState, SimulationMode, TestMode
 )
@@ -31,22 +31,34 @@ from ska_tango_base.control_model import (
 class TestCspSubElementMaster(object):
     """Test case for CSP SubElement Master class."""
 
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-        'PowerDelayStandbyOn': 1.5,
-        'PowerDelayStandbyOff': 1.0,
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = CspSubelementMaster.numpy = MagicMock()
-        # PROTECTED REGION ID(CspSubelementMaster.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  CspSubelementMaster.test_mocking
-
+    @pytest.fixture(scope="class")
+    def device_properties(self):
+        """
+        Fixture that returns device_properties to be provided to the
+        device under test.
+        """
+        return {'PowerDelayStandbyOn': '1.5', 'PowerDelayStandbyOff': '1.0'}
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": CspSubElementMaster,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
+
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # Test the properties
         # PROTECTED REGION ID(CspSubelementMaster.test_properties) ENABLED START #
@@ -75,8 +87,9 @@ class TestCspSubElementMaster(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(CspSubelementMaster.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'CspSubElementMaster, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  CspSubelementMaster.test_GetVersionInfo
@@ -113,7 +126,7 @@ class TestCspSubElementMaster(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(CspSubelementMaster.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  CspSubelementMaster.test_adminMode
 
     # PROTECTED REGION ID(CspSubelementMaster.test_controlMode_decorators) ENABLED START #
@@ -142,20 +155,24 @@ class TestCspSubElementMaster(object):
 
     # PROTECTED REGION ID(CspSubelementMaster.test_powerDelayStandbyOn_decorators) ENABLED START #
     # PROTECTED REGION END #    //  CspSubelementMaster.test_powerDelayStandbyOn_decorators
-    def test_powerDelayStandbyOn(self, tango_context):
+    def test_powerDelayStandbyOn(self, tango_context, device_properties):
         """Test for powerDelayStandbyOn"""
         # PROTECTED REGION ID(CspSubelementMaster.test_testMode) ENABLED START #
-        assert tango_context.device.powerDelayStandbyOn == self.properties['PowerDelayStandbyOn']
+        assert tango_context.device.powerDelayStandbyOn == pytest.approx(
+            float(device_properties['PowerDelayStandbyOn'])
+        )
         tango_context.device.powerDelayStandbyOn = 3
         assert tango_context.device.powerDelayStandbyOn == 3
         # PROTECTED REGION END #    //  CspSubelementMaster.test_powerDelayStandbyOn
 
     # PROTECTED REGION ID(CspSubelementMaster.test_powerDelayStandbyOff_decorators) ENABLED START #
     # PROTECTED REGION END #    //  CspSubelementMaster.test_powerDelayStandbyOff_decorators
-    def test_powerDelayStandbyOff(self, tango_context):
+    def test_powerDelayStandbyOff(self, tango_context, device_properties):
         """Test for powerDelayStandbyOff"""
         # PROTECTED REGION ID(CspSubelementMaster.test_testMode) ENABLED START #
-        assert tango_context.device.powerDelayStandbyOff == self.properties['PowerDelayStandbyOff']
+        assert tango_context.device.powerDelayStandbyOff == pytest.approx(
+            float(device_properties['PowerDelayStandbyOff'])
+        )
         tango_context.device.powerDelayStandbyOff = 2
         assert tango_context.device.powerDelayStandbyOff == 2
         # PROTECTED REGION END #    //  CspSubelementMaster.test_powerDelayStandbyOff
@@ -267,6 +284,7 @@ class TestCspSubElementMaster(object):
         # PROTECTED REGION ID(CspSubelementMaster.test_LoadFirmware) ENABLED START #
         # After initialization the device is in the right state (OFF/MAINTENANCE) to
         # execute the command.
+        tango_context.device.adminMode = AdminMode.MAINTENANCE
         assert tango_context.device.LoadFirmware(['file', 'test/dev/b', '918698a7fea3']) == [
             [ResultCode.OK], ["LoadFirmware command completed OK"]
         ]
@@ -278,13 +296,9 @@ class TestCspSubElementMaster(object):
         """Test for LoadFirmware when the device is in wrong state"""
         # PROTECTED REGION ID(CspSubelementMaster.test_LoadFirmware_when_in_wrong_state) ENABLED START #
         # Set the device in ON/ONLINE state
-        tango_context.device.Disable()
-        tango_context.device.adminMode = AdminMode.ONLINE
-        tango_context.device.Off()
         tango_context.device.On()
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="LoadFirmwareCommand not allowed"):
             tango_context.device.LoadFirmware(['file', 'test/dev/b', '918698a7fea3'])
-        assert "LoadFirmwareCommand not allowed" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementMaster.test_LoadFirmware_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementMaster.test_PowerOnDevices_decorators) ENABLED START #
@@ -304,9 +318,8 @@ class TestCspSubElementMaster(object):
     def test_PowerOnDevices_when_in_wrong_state(self, tango_context):
         """Test for PowerOnDevices when the Master is in wrong state"""
         # PROTECTED REGION ID(CspSubelementMaster.test_PowerOnDevices_when_in_wrong_state) ENABLED START #
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="PowerOnDevicesCommand not allowed"):
             tango_context.device.PowerOnDevices(['test/dev/1', 'test/dev/2'])
-        assert "PowerOnDevicesCommand not allowed" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementMaster.test_PowerOnDevices_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementMaster.test_PowerOffDevices_decorators) ENABLED START #
@@ -326,9 +339,8 @@ class TestCspSubElementMaster(object):
     def test_PowerOffDevices_when_in_wrong_state(self, tango_context):
         """Test for PowerOffDevices when the Master is in wrong state"""
         # PROTECTED REGION ID(CspSubelementMaster.test_PowerOffDevices_when_in_wrong_state) ENABLED START #
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="PowerOffDevicesCommand not allowed"):
             tango_context.device.PowerOffDevices(['test/dev/1', 'test/dev/2'])
-        assert "PowerOffDevicesCommand not allowed" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementMaster.test_PowerOffDevices_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementMaster.test_ReInitDevices_decorators) ENABLED START #
@@ -349,9 +361,8 @@ class TestCspSubElementMaster(object):
         """Test for ReInitDevices whe the device is in a wrong state"""
         # PROTECTED REGION ID(CspSubelementMaster.test_ReInitDevices_when_in_wrong_state) ENABLED START #
         # put it in ON state
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="ReInitDevicesCommand not allowed"):
             tango_context.device.ReInitDevices(['test/dev/1', 'test/dev/2'])
-        assert "ReInitDevicesCommand not allowed" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementMaster.test_ReInitDevices_when_in_wrong_state
 
 
@@ -367,5 +378,5 @@ def test_multiple_devices_in_same_process():
     with MultiDeviceTestContext(devices_info, process=False) as context:
         proxy1 = context.get_device("test/se/1")
         proxy2 = context.get_device("test/master/1")
-        assert proxy1.State() == DevState.OFF
-        assert proxy2.State() == DevState.OFF
+        assert proxy1.State() == DevState.DISABLE
+        assert proxy2.State() == DevState.DISABLE
diff --git a/tests/test_csp_subelement_obsdevice.py b/tests/test_csp_obs_device.py
similarity index 83%
rename from tests/test_csp_subelement_obsdevice.py
rename to tests/test_csp_obs_device.py
index 963b889b9e271396ed7d8d483faf8121a6b61d45..32355164e00ffd78a08bdc34b3143b4b547e15a9 100644
--- a/tests/test_csp_subelement_obsdevice.py
+++ b/tests/test_csp_obs_device.py
@@ -10,7 +10,6 @@
    such device.
 """
 # Imports
-import logging
 import re
 import pytest
 import json
@@ -19,12 +18,14 @@ from tango import DevState, DevFailed
 from tango.test_context import MultiDeviceTestContext
 
 # PROTECTED REGION ID(CspSubelementObsDevice.test_additional_imports) ENABLED START #
-from ska_tango_base import SKAObsDevice, CspSubElementObsDevice, CspSubElementObsDeviceStateModel
+from ska_tango_base import SKAObsDevice, CspSubElementObsDevice
 from ska_tango_base.commands import ResultCode
 from ska_tango_base.control_model import (
     ObsState, AdminMode, ControlMode, HealthState, SimulationMode, TestMode
 )
-from .conftest import load_state_machine_spec, ModelStateMachineTester
+from ska_tango_base.csp import (
+    CspSubElementObsStateModel, ReferenceCspObsComponentManager
+)
 # PROTECTED REGION END #    //  CspSubElementObsDevice.test_additional_imports
 
 
@@ -33,52 +34,45 @@ from .conftest import load_state_machine_spec, ModelStateMachineTester
 # PROTECTED REGION END #    // CspSubelementObsDevice.test_CspSubelementObsDevice_decorators
 
 @pytest.fixture
-def csp_subelement_obsdevice_state_model():
+def csp_subelement_obsdevice_state_model(logger):
     """
     Yields a new CspSubElementObsDevice StateModel for testing
+
+    :param logger: fixture that returns a logger
     """
-    yield CspSubElementObsDeviceStateModel(logging.getLogger())
+    yield CspSubElementObsStateModel(logger)
 
 
-@pytest.mark.state_machine_tester(load_state_machine_spec("csp_subelement_obsdevice_state_machine"))
-class TestCspSubElementObsDeviceStateModel(ModelStateMachineTester):
-    """
-    This class contains the test for the CspSubElementObsDevice StateModel class.
-    """
+class TestCspSubElementObsDevice(object):
+    """Test case for CSP SubElement ObsDevice class."""
 
-    @pytest.fixture
-    def machine(self, csp_subelement_obsdevice_state_model):
+    @pytest.fixture(scope="class")
+    def device_properties(self):
         """
-        Fixture that returns the state machine under test in this class
+        Fixture that returns device_properties to be provided to the
+        device under test.
         """
-        yield csp_subelement_obsdevice_state_model
+        return {'DeviceID': '11'}
 
-    def assert_state(self, machine, state):
-        """
-        Assert the current state of this state machine, based on the
-        values of the adminMode, opState and obsState attributes of this
-        model.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: str
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
         """
-        assert machine.admin_mode == state["admin_mode"]
-        assert machine.op_state == state["op_state"]
-        assert machine.obs_state == state["obs_state"]
-
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
 
-class TestCspSubElementObsDevice(object):
-    """Test case for CSP SubElement ObsDevice class."""
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": CspSubElementObsDevice,
+            "component_manager_patch": lambda self: ReferenceCspObsComponentManager(
+                self.op_state_model, self.obs_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
 
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-        'DeviceID': 11,
-    }
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_State_decorators) ENABLED START #
     # PROTECTED REGION END #    //  CspSubelementObsDevice.test_State_decorators
@@ -102,8 +96,9 @@ class TestCspSubElementObsDevice(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'CspSubElementObsDevice, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_GetVersionInfo
@@ -141,7 +136,7 @@ class TestCspSubElementObsDevice(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_adminMode
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_controlMode_decorators) ENABLED START #
@@ -173,15 +168,18 @@ class TestCspSubElementObsDevice(object):
     def test_scanID(self, tango_context):
         """Test for scanID"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_scanID) ENABLED START #
-        assert tango_context.device.scanID == 0
+        device_under_test = tango_context.device
+        device_under_test.On()
+
+        assert device_under_test.scanID == 0
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_scanID
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_deviceID_decorators) ENABLED START #
     # PROTECTED REGION END #    //  CspSubelementObsDevice.test_deviceID_decorators
-    def test_deviceID(self, tango_context):
+    def test_deviceID(self, tango_context, device_properties):
         """Test for deviceID"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_scanID) ENABLED START #
-        assert tango_context.device.deviceID == self.properties['DeviceID']
+        assert tango_context.device.deviceID == int(device_properties['DeviceID'])
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_scanID
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_sdpDestinationAddresses_decorators) ENABLED START #
@@ -225,13 +223,17 @@ class TestCspSubElementObsDevice(object):
     def test_ConfigureScan(self, tango_context, tango_change_event_helper):
         """Test for ConfigureScan"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan) ENABLED START #
-        tango_context.device.On()
+        device_under_test = tango_context.device
+        device_under_test.On()
+        assert device_under_test.obsState == ObsState.IDLE
+
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         scan_configuration = '{"id":"sbi-mvp01-20200325-00002"}'
-        tango_context.device.ConfigureScan(scan_configuration)
-        obs_state_callback.assert_calls([ObsState.IDLE, ObsState.CONFIGURING])
-        assert tango_context.device.configurationID == "sbi-mvp01-20200325-00002"
-        assert tango_context.device.lastScanConfiguration == scan_configuration
+        device_under_test.ConfigureScan(scan_configuration)
+        obs_state_callback.assert_calls([ObsState.IDLE, ObsState.CONFIGURING, ObsState.READY])
+        assert device_under_test.obsState == ObsState.READY
+        assert device_under_test.configurationID == "sbi-mvp01-20200325-00002"
+        assert device_under_test.lastScanConfiguration == scan_configuration
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ConfigureScan
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_when_in_wrong_state_decorators) ENABLED START #
@@ -240,10 +242,10 @@ class TestCspSubElementObsDevice(object):
         """Test for ConfigureScan when the device is in wrong state"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_when_in_wrong_state) ENABLED START #
         # The device in in OFF/IDLE state, not valid to invoke ConfigureScan.
-        with pytest.raises(DevFailed) as df:
-            tango_context.device.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
-        assert "Error executing command ConfigureScanCommand" in str(
-            df.value.args[0].desc)
+        device_under_test = tango_context.device
+
+        with pytest.raises(DevFailed, match="Component is not ON"):
+            device_under_test.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ConfigureScan_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_with_wrong_input_args_decorators) ENABLED START #
@@ -255,9 +257,12 @@ class TestCspSubElementObsDevice(object):
         # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_with_wrong_input_args_when_idle) ENABLED START #
         tango_context.device.On()
         # wrong configurationID key
+        assert tango_context.device.obsState == ObsState.IDLE
+
         wrong_configuration = '{"subid":"sbi-mvp01-20200325-00002"}'
-        tango_context.device.ConfigureScan(wrong_configuration)
-        assert tango_context.device.obsState == ObsState.FAULT
+        (result_code, _) = tango_context.device.ConfigureScan(wrong_configuration)
+        assert result_code == ResultCode.FAILED
+        assert tango_context.device.obsState == ObsState.IDLE
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ConfigureScan_with_wrong_input_args
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_with_json_syntax_error) ENABLED START #
@@ -266,8 +271,11 @@ class TestCspSubElementObsDevice(object):
         """Test for ConfigureScan when syntax error in json configuration """
         # PROTECTED REGION ID(CspSubelementObsDevice.test_ConfigureScan_with_json_syntax_error) ENABLED START #
         tango_context.device.On()
-        tango_context.device.ConfigureScan('{"foo": 1,}')
-        assert tango_context.device.obsState == ObsState.FAULT
+        assert tango_context.device.obsState == ObsState.IDLE
+
+        (result_code, _) = tango_context.device.ConfigureScan('{"foo": 1,}')
+        assert result_code == ResultCode.FAILED
+        assert tango_context.device.obsState == ObsState.IDLE
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ConfigureScan_with_json_syntax_error
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_GoToIdle_decorators) ENABLED START #
@@ -292,9 +300,9 @@ class TestCspSubElementObsDevice(object):
         """Test for GoToIdle when the device is in wrong state"""
         # PROTECTED REGION ID(CspSubelementObsDevice.test_GoToIdle_when_in_wrong_state) ENABLED START #
         # The device in in OFF/IDLE state, not valid to invoke GoToIdle.
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="Command not permitted by state model."):
             tango_context.device.GoToIdle()
-        assert "Error executing command GoToIdleCommand" in str(df.value.args[0].desc)
+        
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_GoToIdle_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_Scan_decorators) ENABLED START #
@@ -317,9 +325,8 @@ class TestCspSubElementObsDevice(object):
         # PROTECTED REGION ID(CspSubelementObsDevice.test_Scan_when_in_wrong_state) ENABLED START #
         # Set the device in ON/IDLE state
         tango_context.device.On()
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="Command not permitted by state model."):
             tango_context.device.Scan('32')
-        assert "Error executing command ScanCommand" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_Scan_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_Scan_with_wrong_argument_decorators) ENABLED START #
@@ -330,8 +337,9 @@ class TestCspSubElementObsDevice(object):
         # Set the device in ON/IDLE state
         tango_context.device.On()
         tango_context.device.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
-        tango_context.device.Scan('abc')
-        assert tango_context.device.obsState == ObsState.FAULT
+        (result_code, _) = tango_context.device.Scan('abc')
+        assert result_code == ResultCode.FAILED
+        assert tango_context.device.obsState == ObsState.READY
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_Scan_with_wrong_argument
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_EndScan_decorators) ENABLED START #
@@ -357,9 +365,9 @@ class TestCspSubElementObsDevice(object):
         # Set the device in ON/READY state
         tango_context.device.On()
         tango_context.device.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="Command not permitted by state model."):
             tango_context.device.EndScan()
-        assert "Error executing command EndScanCommand" in str(df.value.args[0].desc)
+        
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_EndScan_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_ObsReset_decorators) ENABLED START #
@@ -374,7 +382,7 @@ class TestCspSubElementObsDevice(object):
         tango_context.device.Abort()
         obs_state_callback.assert_calls([ObsState.ABORTING, ObsState.ABORTED])
         tango_context.device.ObsReset()
-        obs_state_callback.assert_call(ObsState.IDLE)
+        obs_state_callback.assert_calls([ObsState.RESETTING, ObsState.IDLE])
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ObsReset
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_ObsReset_when_in_wrong_state_decorators) ENABLED START #
@@ -384,9 +392,8 @@ class TestCspSubElementObsDevice(object):
         # PROTECTED REGION ID(CspSubelementObsDevice.test_ObsReset_when_in_wrong_state) ENABLED START #
         # Set the device in ON/IDLE state
         tango_context.device.On()
-        with pytest.raises(DevFailed) as df:
+        with pytest.raises(DevFailed, match="Command not permitted by state model."):
             tango_context.device.ObsReset()
-        assert "Error executing command ObsResetCommand" in str(df.value.args[0].desc)
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_ObsReset_when_in_wrong_state
 
     # PROTECTED REGION ID(CspSubelementObsDevice.test_Abort_decorators) ENABLED START #
@@ -402,16 +409,6 @@ class TestCspSubElementObsDevice(object):
             [ObsState.READY, ObsState.ABORTING, ObsState.ABORTED])
         # PROTECTED REGION END #    //  CspSubelementObsDevice.test_Abort
 
-    # PROTECTED REGION ID(CspSubelementObsDevice.test_Abort_when_in_wrong_state_decorators) ENABLED START #
-    # PROTECTED REGION END #    //  CspSubelementObsDevice.test_Abort_when_in_wrong_state_decorators
-    def test_Abort_when_in_wrong_state(self, tango_context):
-        """Test for Abort when the device is in wrong state"""
-        # PROTECTED REGION ID(CspSubelementObsDevice.test_Abort_when_in_wrong_state) ENABLED START #
-        with pytest.raises(DevFailed) as df:
-            tango_context.device.Abort()
-        assert "Error executing command AbortCommand" in str(df.value.args[0].desc)
-        # PROTECTED REGION END #    //  CspSubelementObsDevice.test_Abort_when_in_wrong_state
-
 
 def test_multiple_devices_in_same_process():
     devices_info = (
@@ -422,5 +419,5 @@ def test_multiple_devices_in_same_process():
     with MultiDeviceTestContext(devices_info, process=False) as context:
         proxy1 = context.get_device("test/se/1")
         proxy2 = context.get_device("test/obsdevice/1")
-        assert proxy1.State() == DevState.OFF
-        assert proxy2.State() == DevState.OFF
+        assert proxy1.State() == DevState.DISABLE
+        assert proxy2.State() == DevState.DISABLE
diff --git a/tests/test_csp_subelement_subarray.py b/tests/test_csp_subarray.py
similarity index 88%
rename from tests/test_csp_subelement_subarray.py
rename to tests/test_csp_subarray.py
index fd5ca072adba85855fcdbdf993d99dc1e05801a1..ad33e26ea5628f4dc2cfe4fc3f7a7cfa5689e088 100644
--- a/tests/test_csp_subelement_subarray.py
+++ b/tests/test_csp_subarray.py
@@ -10,7 +10,6 @@
    such device.
 """
 # Imports
-import logging
 import re
 import pytest
 import json
@@ -18,13 +17,13 @@ import json
 from tango import DevState, DevFailed
 
 # PROTECTED REGION ID(CspSubelementSubarray.test_additional_imports) ENABLED START #
-from ska_tango_base import SKASubarray, CspSubElementSubarray
 from ska_tango_base.commands import ResultCode
-from ska_tango_base.faults import StateModelError
 from ska_tango_base.control_model import (
     ObsState, AdminMode, ControlMode, HealthState, SimulationMode, TestMode
 )
-from .conftest import load_state_machine_spec, StateMachineTester
+from ska_tango_base.csp import (
+    CspSubElementSubarray, ReferenceCspSubarrayComponentManager
+)
 # PROTECTED REGION END #    //  CspSubElementSubarray.test_additional_imports
 
 
@@ -35,19 +34,35 @@ from .conftest import load_state_machine_spec, StateMachineTester
 class TestCspSubElementSubarray(object):
     """Test case for CSP SubElement Subarray class."""
 
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-    }
+    @pytest.fixture(scope="class")
+    def device_properties(self):
+        """
+        Fixture that returns device_properties to be provided to the
+        device under test.
+        """
+        return {"CapabilityTypes": ["id"]}
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
 
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = CspSubelementSubarray.numpy = MagicMock()
-        # PROTECTED REGION ID(CspSubelementSubarray.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  CspSubelementSubarray.test_mocking
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": CspSubElementSubarray,
+            "component_manager_patch": lambda self: ReferenceCspSubarrayComponentManager(
+                self.op_state_model,
+                self.obs_state_model,
+                self.CapabilityTypes,
+                logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
 
     @pytest.mark.skip(reason="Not implemented")
     def test_properties(self, tango_context):
@@ -78,8 +93,9 @@ class TestCspSubElementSubarray(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'CspSubElementSubarray, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_GetVersionInfo
@@ -116,7 +132,7 @@ class TestCspSubElementSubarray(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_adminMode
 
     # PROTECTED REGION ID(CspSubelementSubarray.test_controlMode_decorators) ENABLED START #
@@ -148,7 +164,9 @@ class TestCspSubElementSubarray(object):
     def test_scanID(self, tango_context):
         """Test for scanID"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_scanID) ENABLED START #
-        assert tango_context.device.scanID == 0
+        device_under_test = tango_context.device
+        device_under_test.On()
+        assert device_under_test.scanID == 0
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_scanID
 
     # PROTECTED REGION ID(CspSubelementSubarray.test_sdpDestinationAddresses_decorators) ENABLED START #
@@ -288,12 +306,16 @@ class TestCspSubElementSubarray(object):
     def test_ConfigureScan(self, tango_context, tango_change_event_helper, command_alias):
         """Test for ConfigureScan"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_ConfigureScan) ENABLED START #
-        tango_context.device.On()
-        tango_context.device.AssignResources('{"example": [1,2,3]}')
+        device_under_test = tango_context.device
+        device_under_test.On()
+        device_under_test.AssignResources(json.dumps([1,2,3]))
+        assert device_under_test.obsState == ObsState.IDLE
+
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         scan_configuration = '{"id":"sbi-mvp01-20200325-00002"}'
-        tango_context.device.command_inout(command_alias, (scan_configuration))
-        obs_state_callback.assert_calls([ObsState.IDLE, ObsState.CONFIGURING])
+        device_under_test.command_inout(command_alias, scan_configuration)
+        obs_state_callback.assert_calls([ObsState.IDLE, ObsState.CONFIGURING, ObsState.READY])
+        assert device_under_test.obsState == ObsState.READY
         assert tango_context.device.configurationID == "sbi-mvp01-20200325-00002"
         assert tango_context.device.lastScanConfiguration == scan_configuration
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_ConfigureScan
@@ -304,7 +326,7 @@ class TestCspSubElementSubarray(object):
         """Test for ConfigureScan when the device is in wrong state"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_ConfigureScan_when_in_wrong_state) ENABLED START #
         # The device in in OFF/EMPTY state, not valid to invoke ConfigureScan.
-        with pytest.raises(DevFailed, match="Error executing command ConfigureScanCommand"):
+        with pytest.raises(DevFailed, match="Command not permitted by state model."):
             tango_context.device.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_ConfigureScan_when_in_wrong_state
 
@@ -316,11 +338,14 @@ class TestCspSubElementSubarray(object):
         """
         # PROTECTED REGION ID(CspSubelementSubarray.test_ConfigureScan_with_wrong_configId_key) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": [1,2,3]}')
+        tango_context.device.AssignResources(json.dumps([1,2,3]))
         # wrong configurationID key
+        assert tango_context.device.obsState == ObsState.IDLE
+
         wrong_configuration = '{"subid":"sbi-mvp01-20200325-00002"}'
-        result_code, msg = tango_context.device.ConfigureScan(wrong_configuration)
+        result_code, _ = tango_context.device.ConfigureScan(wrong_configuration)
         assert result_code == ResultCode.FAILED
+        assert tango_context.device.obsState == ObsState.IDLE
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_ConfigureScan_with_wrong_configId_key
 
     # PROTECTED REGION ID(CspSubelementSubarray.test_ConfigureScan_with_json_syntax_error) ENABLED START #
@@ -329,9 +354,12 @@ class TestCspSubElementSubarray(object):
         """Test for ConfigureScan when syntax error in json configuration """
         # PROTECTED REGION ID(CspSubelementSubarray.test_ConfigureScan_with_json_syntax_error) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": [1,2,3]}')
-        result_code, msg = tango_context.device.ConfigureScan('{"foo": 1,}')
+        tango_context.device.AssignResources(json.dumps([1,2,3]))
+        assert tango_context.device.obsState == ObsState.IDLE
+
+        result_code, _ = tango_context.device.ConfigureScan('{"foo": 1,}')
         assert result_code == ResultCode.FAILED
+        assert tango_context.device.obsState == ObsState.IDLE
         # PROTECTED REGION END #    //  CspSubelementSubarray.test_ConfigureScan_with_json_syntax_error
 
     # PROTECTED REGION ID(CspSubelementSubarray.test_GoToIdle_decorators) ENABLED START #
@@ -341,7 +369,7 @@ class TestCspSubElementSubarray(object):
         """Test for GoToIdle"""
         # PROTECTED REGION ID(CspSubelementSubarray.test_GoToIdle) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": [1,2,3]}')
+        tango_context.device.AssignResources(json.dumps([1,2,3]))
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         tango_context.device.ConfigureScan('{"id":"sbi-mvp01-20200325-00002"}')
         obs_state_callback.assert_calls(
diff --git a/tests/test_csp_subarray_component_manager.py b/tests/test_csp_subarray_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..589d1ffd561d73d4c8afa09f9d2d4ec128f8be1c
--- /dev/null
+++ b/tests/test_csp_subarray_component_manager.py
@@ -0,0 +1,488 @@
+"""
+Tests for the
+:py:mod:`ska_tango_base.csp_subelement_component_manager`
+module.
+"""
+import contextlib
+import itertools
+
+import pytest
+
+from ska_tango_base.faults import ComponentError, ComponentFault
+from ska_tango_base.csp import ReferenceCspSubarrayComponentManager
+from ska_tango_base.control_model import PowerMode
+
+
+class TestCspSubelementSubarrayComponentManager:
+    """
+    Tests of the
+    :py:class:`ska_tango_base.csp_subelement_component_manager.SubarrayComponentManager`
+    class.
+    """
+
+    @pytest.fixture()
+    def mock_op_state_model(self, mocker):
+        """
+        Fixture that returns a mock op state model
+
+        :param mocker: pytest fixture that wraps
+            :py:mod:`unittest.mock`.
+
+        :return: a mock state model
+        """
+        return mocker.Mock()
+
+    @pytest.fixture()
+    def mock_obs_state_model(self, mocker):
+        """
+        Fixture that returns a mock observation state model
+
+        :param mocker: pytest fixture that wraps
+            :py:mod:`unittest.mock`.
+
+        :return: a mock state model
+        """
+        return mocker.Mock()
+
+    @pytest.fixture()
+    def mock_resource_factory(self, mocker):
+        return mocker.Mock
+
+    @pytest.fixture()
+    def mock_capability_types(self, mocker):
+        return ["foo", "bah"]
+
+    @pytest.fixture()
+    def mock_config_factory(self):
+        mock_config_generator = ({"id": f"mock_id_{i}"} for i in itertools.count(1))
+        return lambda: next(mock_config_generator)
+
+    @pytest.fixture()
+    def mock_scan_args(self, mocker):
+        return mocker.Mock()
+
+    @pytest.fixture(params=[PowerMode.OFF, PowerMode.STANDBY, PowerMode.ON])
+    def initial_power_mode(self, request):
+        return request.param
+
+    @pytest.fixture(params=[False, True])
+    def initial_fault(self, request):
+        return request.param
+
+    @pytest.fixture()
+    def component(self, mock_capability_types, initial_power_mode, initial_fault):
+        return ReferenceCspSubarrayComponentManager._Component(
+            mock_capability_types, _power_mode=initial_power_mode, _faulty=initial_fault
+        )
+
+    @pytest.fixture()
+    def component_manager(
+        self,
+        mock_op_state_model,
+        mock_obs_state_model,
+        mock_capability_types,
+        logger,
+        component,
+    ):
+        """
+        Fixture that returns the component manager under test
+
+        :param mock_op_state_model: a mock state model for testing
+        :param logger: a logger for the component manager
+
+        :return: the component manager under test
+        """
+        return ReferenceCspSubarrayComponentManager(
+            mock_op_state_model,
+            mock_obs_state_model,
+            mock_capability_types,
+            logger=logger,
+            _component=component,
+        )
+
+    def test_state_changes_with_connect_and_disconnect(
+        self, component_manager, mock_op_state_model, initial_power_mode, initial_fault
+    ):
+        """
+        Test that the state model is updated with state changes when the
+        component manager connects to and disconnects from its
+        component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+        expected_action = (
+            "component_fault" if initial_fault else power_mode_map[initial_power_mode]
+        )
+
+        assert not component_manager.is_communicating
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+        assert mock_op_state_model.perform_action.call_args_list == [
+            (("component_unknown",),),
+            ((expected_action,),),
+        ]
+
+        mock_op_state_model.reset_mock()
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with(
+            "component_disconnected"
+        )
+
+    def test_simulate_communication_failure(self, component_manager, mock_op_state_model):
+        """
+        Test that we can simulate connection failure.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        mock_op_state_model.reset_mock()
+        component_manager.simulate_communication_failure(True)
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with("component_unknown")
+
+        with pytest.raises(ConnectionError, match="Failed to connect"):
+            component_manager.start_communicating()
+
+    @pytest.mark.parametrize("command", ["off", "standby", "on"])
+    def test_base_command_fails_when_disconnected(self, component_manager, command):
+        """
+        Test that component commands fail when the component manager
+        isn't connected to the component.
+
+        :param component_manager: the component manager under test
+        :param command: the command under test
+        """
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+    @pytest.mark.parametrize(
+        ("command", "action"),
+        [
+            ("off", "component_off"),
+            ("standby", "component_standby"),
+            ("on", "component_on"),
+        ],
+    )
+    def test_base_command_succeeds_when_connected(
+        self,
+        component_manager,
+        mock_op_state_model,
+        initial_power_mode,
+        initial_fault,
+        command,
+        action,
+    ):
+        """
+        Test that component commands succeed when the component manager
+        is connected to the component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param command: the name of the command under test
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component_manager, command)()
+
+            if power_mode_map[initial_power_mode] == action:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    @pytest.mark.parametrize(
+        ("call", "action"),
+        [
+            ("simulate_off", "component_off"),
+            ("simulate_standby", "component_standby"),
+            ("simulate_on", "component_on"),
+        ],
+    )
+    def test_simulated_change_propagates(
+        self,
+        component_manager,
+        mock_op_state_model,
+        component,
+        initial_power_mode,
+        initial_fault,
+        call,
+        action,
+    ):
+        """
+        Test that spontaneous changes to the state of the component
+        result in the correct action being performed on the state model.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param state:
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component, call)()
+            if action == power_mode_map[initial_power_mode]:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    def test_reset_from_fault(
+        self, component_manager, mock_op_state_model, initial_fault
+    ):
+        component_manager.start_communicating()
+        assert component_manager.faulty == initial_fault
+        mock_op_state_model.reset_mock()
+
+        component_manager.reset()
+        assert not component_manager.faulty
+        mock_op_state_model.perform_action.assert_not_called()
+
+    def test_assign(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource_1 = mock_resource_factory()
+        mock_resource_2 = mock_resource_factory()
+
+        component_manager.assign([mock_resource_1])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_resourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.assign([mock_resource_2])
+        mock_obs_state_model.perform_action.assert_not_called()
+
+        component_manager.release([mock_resource_1])
+        mock_obs_state_model.perform_action.assert_not_called()
+
+        component_manager.release([mock_resource_2])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_unresourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.assign([mock_resource_1, mock_resource_2])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_resourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.release_all()
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_unresourced"
+        )
+
+    def test_configure(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration_1 = mock_config_factory()
+        mock_configuration_2 = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_resourced"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.configure(mock_configuration_1)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_configured"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.configure(mock_configuration_2)
+            mock_obs_state_model.perform_action.assert_not_called()
+
+            component_manager.deconfigure()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_unconfigured"
+            )
+            mock_obs_state_model.reset_mock()
+
+    def test_scan(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+        mock_scan_args,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            component_manager.configure(mock_configuration)
+            mock_obs_state_model.reset_mock()
+
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.end_scan()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_not_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_scan_stopped()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_not_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+    def test_obsfault_reset_restart(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+        mock_scan_args,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            component_manager.configure(mock_configuration)
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(True)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_obsfault"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(False)
+            component_manager.obsreset()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_unconfigured"
+            )
+
+            component_manager.configure(mock_configuration)
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(True)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_obsfault"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(False)
+            component_manager.restart()
+            assert mock_obs_state_model.perform_action.call_args_list == [
+                (("component_unconfigured",),),
+                (("component_unresourced",),),
+            ]
diff --git a/tests/test_csp_subelement_obsdev_state_machine.py b/tests/test_csp_subelement_obsdev_state_machine.py
deleted file mode 100644
index 77df307cf880279bde31e44ab9e28c227b776847..0000000000000000000000000000000000000000
--- a/tests/test_csp_subelement_obsdev_state_machine.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-Module to test the transitions of a CSP SubElement ObsDevice
-(ska_tango_base.csp_subelement_obsdev_state_machine module).
-"""
-import pytest
-
-from ska_tango_base.csp_subelement_state_machine import (
-    CspSubElementObsDeviceStateMachine,
-)
-from .conftest import load_state_machine_spec, TransitionsStateMachineTester
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("csp_subelement_obsdev_transitions"))
-class TestCspSubElementObsDeviceStateMachine(TransitionsStateMachineTester):
-    """
-    This class contains the test for the CspSubElementObsDeviceStateMachine class.
-    """
-
-    @pytest.fixture
-    def machine(self):
-        """
-        Fixture that returns the state machine under test in this class
-
-        :yields: the state machine under test
-        """
-        yield CspSubElementObsDeviceStateMachine()
diff --git a/tests/test_logger_device.py b/tests/test_logger_device.py
index f035c46833979642f7f920b4dfe3cde98f76319a..47cde803e03f1e43f7a525618abee2d747a1ebc6 100644
--- a/tests/test_logger_device.py
+++ b/tests/test_logger_device.py
@@ -12,8 +12,9 @@ import re
 import pytest
 from tango import DevState
 from tango.test_context import MultiDeviceTestContext
+from ska_tango_base.base import ReferenceBaseComponentManager
 from ska_tango_base.logger_device import SKALogger
-from ska_tango_base.subarray_device import SKASubarray
+from ska_tango_base.subarray import SKASubarray
 import tango
 
 # PROTECTED REGION ID(SKALogger.test_additional_imports) ENABLED START #
@@ -33,21 +34,26 @@ from ska_tango_base.control_model import (
 @pytest.mark.usefixtures("tango_context", "initialize_device")
 # PROTECTED REGION END #    //  SKALogger.test_SKALogger_decorators
 class TestSKALogger(object):
-    """Test case for packet generation."""
-
-    properties = {
-        "SkaLevel": "1",
-        "GroupDefinitions": "",
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKALogger.numpy = MagicMock()
-        # PROTECTED REGION ID(SKALogger.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKALogger.test_mocking
-
+    """
+    Test class for tests of the SKALogger device class.
+    """
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+        """
+        return {
+            "device": SKALogger,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
+
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # test the properties
         # PROTECTED REGION ID(SKALogger.test_properties) ENABLED START #
@@ -76,8 +82,8 @@ class TestSKALogger(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKALogger.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r"SKALogger, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, "
-            r"A set of generic base devices for SKA Telescope."
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
         )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
@@ -125,7 +131,7 @@ class TestSKALogger(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKALogger.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  SKALogger.test_adminMode
 
     # PROTECTED REGION ID(SKALogger.test_controlMode_decorators) ENABLED START #
diff --git a/tests/test_master_device.py b/tests/test_master_device.py
index a9c9b45d23064fcdbd3361c09eed096171a01ef7..49d8dbab79ebfadc194dcd69910c7e172f4c39d1 100644
--- a/tests/test_master_device.py
+++ b/tests/test_master_device.py
@@ -13,6 +13,8 @@ import pytest
 from tango import DevState
 
 # PROTECTED REGION ID(SKAMaster.test_additional_imports) ENABLED START #
+from ska_tango_base import SKAMaster
+from ska_tango_base.base import ReferenceBaseComponentManager
 from ska_tango_base.control_model import AdminMode, ControlMode, HealthState, SimulationMode, TestMode
 # PROTECTED REGION END #    //  SKAMaster.test_additional_imports
 
@@ -21,26 +23,49 @@ from ska_tango_base.control_model import AdminMode, ControlMode, HealthState, Si
 @pytest.mark.usefixtures("tango_context")
 # PROTECTED REGION END #    //  SKAMaster.test_SKAMaster_decorators
 class TestSKAMaster(object):
-    """Test case for packet generation."""
-
-    capabilities = ['BAND1:1', 'BAND2:1', 'BAND3:0', 'BAND4:0', 'BAND5:0']
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-        'NrSubarrays': '16',
-        'CapabilityTypes': '',
-        'MaxCapabilities': 'BAND1:1'
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKAMaster.numpy = MagicMock()
-        # PROTECTED REGION ID(SKAMaster.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKAMaster.test_mocking
-
+    """
+    Test class for tests of the SKAMaster device class.
+    """
+
+    # capabilities = ['BAND1:1', 'BAND2:1', 'BAND3:0', 'BAND4:0', 'BAND5:0']
+
+    @pytest.fixture(scope="class")
+    def device_properties(self):
+        """
+        Fixture that returns device_properties to be provided to the
+        device under test.
+        """
+        return {
+            'SkaLevel': '4',
+            'LoggingTargetsDefault': '',
+            'GroupDefinitions': '',
+            'NrSubarrays': '16',
+            'CapabilityTypes': '',
+            'MaxCapabilities': ['BAND1:1', 'BAND2:1']
+        }
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": SKAMaster,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
+
+
+
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # Test the properties
         # PROTECTED REGION ID(SKAMaster.test_properties) ENABLED START #
@@ -69,8 +94,9 @@ class TestSKAMaster(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKAMaster.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKAMaster, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKAMaster.test_GetVersionInfo
@@ -160,7 +186,7 @@ class TestSKAMaster(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKAMaster.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  SKAMaster.test_adminMode
 
     # PROTECTED REGION ID(SKAMaster.test_controlMode_decorators) ENABLED START #
diff --git a/tests/test_obs_device.py b/tests/test_obs_device.py
index 7a6c230309142c3f35c013c72efd9af05c041064..c19adf7e8351878ce963554fbb37ca54270ca064 100644
--- a/tests/test_obs_device.py
+++ b/tests/test_obs_device.py
@@ -17,6 +17,7 @@ from tango.test_context import MultiDeviceTestContext
 
 # PROTECTED REGION ID(SKAObsDevice.test_additional_imports) ENABLED START #
 from ska_tango_base import SKABaseDevice, SKAObsDevice
+from ska_tango_base.base import ReferenceBaseComponentManager
 from ska_tango_base.control_model import (
     AdminMode, ControlMode, HealthState, ObsMode, ObsState, SimulationMode, TestMode
 )
@@ -28,22 +29,31 @@ from ska_tango_base.control_model import (
 @pytest.mark.usefixtures("tango_context", "initialize_device")
 # PROTECTED REGION END #    //  SKAObsDevice.test_SKAObsDevice_decorators
 class TestSKAObsDevice(object):
-    """Test case for packet generation."""
-
-    properties = {
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'GroupDefinitions': '',
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKAObsDevice.numpy = MagicMock()
-        # PROTECTED REGION ID(SKAObsDevice.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKAObsDevice.test_mocking
-
+    """
+    Test class for tests of the SKAObsDevice device class.
+    """
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": SKAObsDevice,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
+
+
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # Test the properties
         # PROTECTED REGION ID(SKAObsDevice.test_properties) ENABLED START #
@@ -72,8 +82,9 @@ class TestSKAObsDevice(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKAObsDevice.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKAObsDevice, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKAObsDevice.test_GetVersionInfo
@@ -148,7 +159,7 @@ class TestSKAObsDevice(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKAObsDevice.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  SKAObsDevice.test_adminMode
 
     # PROTECTED REGION ID(SKAObsDevice.test_controlMode_decorators) ENABLED START #
@@ -188,5 +199,5 @@ def test_multiple_devices_in_same_process():
     with MultiDeviceTestContext(devices_info, process=False) as context:
         proxy1 = context.get_device("test/obs/1")
         proxy2 = context.get_device("test/base/1")
-        assert proxy1.State() == DevState.OFF
-        assert proxy2.State() == DevState.OFF
+        assert proxy1.State() == DevState.DISABLE
+        assert proxy2.State() == DevState.DISABLE
diff --git a/tests/test_state_machines.py b/tests/test_state_machines.py
deleted file mode 100644
index 8eaa2aeedb1300abfcfeb3792935efcedc53d886..0000000000000000000000000000000000000000
--- a/tests/test_state_machines.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""
-This module contains the tests for the ska_tango_base.state_machine module.
-"""
-import pytest
-
-from ska_tango_base.state_machine import (
-    AdminModeStateMachine,
-    OperationStateMachine,
-    ObservationStateMachine,
-)
-from .conftest import load_state_machine_spec, TransitionsStateMachineTester
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("operation_state_machine"))
-class TestOperationStateMachine(TransitionsStateMachineTester):
-    """
-    This class contains the test for the DeviceStateMachine class.
-    """
-
-    @pytest.fixture
-    def machine(self):
-        """
-        Fixture that returns the state machine under test in this class
-
-        :yields: the state machine under test
-        """
-        yield OperationStateMachine()
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("admin_mode_state_machine"))
-class TestAdminModeStateMachine(TransitionsStateMachineTester):
-    """
-    This class contains the test for the DeviceStateMachine class.
-    """
-
-    @pytest.fixture
-    def machine(self):
-        """
-        Fixture that returns the state machine under test in this class
-
-        :yields: the state machine under test
-        """
-        yield AdminModeStateMachine()
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("observation_state_machine"))
-class TestObservationStateMachine(TransitionsStateMachineTester):
-    """
-    This class contains the test for the ObservationStateMachine class.
-    """
-
-    @pytest.fixture
-    def machine(self):
-        """
-        Fixture that returns the state machine under test in this class
-
-        :yields: the state machine under test
-        """
-        yield ObservationStateMachine()
diff --git a/tests/test_subarray_component_manager.py b/tests/test_subarray_component_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee9e0c8a5e4d4adf22ff66b57d04031e7dd02faa
--- /dev/null
+++ b/tests/test_subarray_component_manager.py
@@ -0,0 +1,607 @@
+"""
+Tests for the :py:mod:`ska_tango_base.component_manager`
+module.
+"""
+import contextlib
+import itertools
+
+import pytest
+
+# from tango import DevState
+
+from ska_tango_base.faults import ComponentError, ComponentFault
+from ska_tango_base.subarray import ReferenceSubarrayComponentManager
+from ska_tango_base.control_model import PowerMode
+
+
+class TestSubarrayComponentResourceManager:
+    """
+    Test suite for the Subarray._Component._ResourceManager
+    """
+
+    @pytest.fixture
+    def mock_callback(self, mocker):
+        return mocker.Mock()
+
+    @pytest.fixture
+    def resource_pool(self, mock_callback):
+        """
+        Fixture that yields the component's resource manager
+
+        :return:
+            :py:class:`SubarrayComponentManager._ResourcePool`
+        """
+        return ReferenceSubarrayComponentManager._ResourcePool(mock_callback)
+
+    def test_ResourceManager_assign(self, resource_pool, mock_callback):
+        """
+        Test that the ResourceManager assigns resource correctly.
+        """
+        # create a resource manager and check that it is empty
+        assert not len(resource_pool)
+        assert resource_pool.get() == set()
+
+        resource_pool.assign(["A"])
+        mock_callback.assert_called_once_with(True)
+        assert len(resource_pool) == 1
+        assert resource_pool.get() == set(["A"])
+        mock_callback.reset_mock()
+
+        resource_pool.assign(["A"])
+        assert len(resource_pool) == 1
+        assert resource_pool.get() == set(["A"])
+        mock_callback.assert_not_called()
+
+        resource_pool.assign(["A", "B"])
+        assert len(resource_pool) == 2
+        assert resource_pool.get() == set(["A", "B"])
+        mock_callback.assert_not_called()
+
+        resource_pool.assign(["A"])
+        assert len(resource_pool) == 2
+        assert resource_pool.get() == set(["A", "B"])
+        mock_callback.assert_not_called()
+
+        resource_pool.assign(["A", "C"])
+        assert len(resource_pool) == 3
+        assert resource_pool.get() == set(["A", "B", "C"])
+        mock_callback.assert_not_called()
+
+        resource_pool.assign(["D"])
+        assert len(resource_pool) == 4
+        assert resource_pool.get() == set(["A", "B", "C", "D"])
+        mock_callback.assert_not_called()
+
+    def test_ResourceManager_release(self, resource_pool, mock_callback):
+        """
+        Test that the ResourceManager releases resource correctly.
+        """
+        resource_pool.assign(["A", "B", "C", "D"])
+        mock_callback.assert_called_once_with(True)
+        mock_callback.reset_mock()
+
+        # okay to release resources not assigned; does nothing
+        resource_pool.release(["E"])
+        assert len(resource_pool) == 4
+        assert resource_pool.get() == set(["A", "B", "C", "D"])
+        mock_callback.assert_not_called()
+
+        # check release does what it should
+        resource_pool.release(["D"])
+        assert len(resource_pool) == 3
+        assert resource_pool.get() == set(["A", "B", "C"])
+        mock_callback.assert_not_called()
+
+        # okay to release resources both assigned and not assigned
+        resource_pool.release(["C", "D"])
+        assert len(resource_pool) == 2
+        assert resource_pool.get() == set(["A", "B"])
+        mock_callback.assert_not_called()
+
+        # check release all does what it should
+        resource_pool.release_all()
+        assert len(resource_pool) == 0
+        assert resource_pool.get() == set()
+        mock_callback.assert_called_once_with(False)
+        mock_callback.reset_mock()
+
+        # okay to call release_all when already empty
+        resource_pool.release_all()
+        assert len(resource_pool) == 0
+        assert resource_pool.get() == set()
+        mock_callback.assert_not_called()
+
+
+class TestSubarrayComponentManager:
+    """
+    Tests of the
+    :py:class:`ska_tango_base.component_manager.SubarrayComponentManager`
+    class.
+    """
+
+    @pytest.fixture()
+    def mock_op_state_model(self, mocker):
+        """
+        Fixture that returns a mock op state model
+
+        :param mocker: pytest fixture that wraps
+            :py:mod:`unittest.mock`.
+
+        :return: a mock state model
+        """
+        return mocker.Mock()
+
+    @pytest.fixture()
+    def mock_obs_state_model(self, mocker):
+        """
+        Fixture that returns a mock observation state model
+
+        :param mocker: pytest fixture that wraps
+            :py:mod:`unittest.mock`.
+
+        :return: a mock state model
+        """
+        return mocker.Mock()
+
+    @pytest.fixture()
+    def mock_resource_factory(self, mocker):
+        return mocker.Mock
+
+    @pytest.fixture()
+    def mock_capability_types(self, mocker):
+        return ["foo", "bah"]
+
+    @pytest.fixture()
+    def mock_config_factory(self):
+        mock_config_generator = ({"foo": i, "bah": i} for i in itertools.count(1))
+        return lambda: next(mock_config_generator)
+
+    @pytest.fixture()
+    def mock_scan_args(self, mocker):
+        return mocker.Mock()
+
+    @pytest.fixture(params=[PowerMode.OFF, PowerMode.STANDBY, PowerMode.ON])
+    def initial_power_mode(self, request):
+        return request.param
+
+    @pytest.fixture(params=[False, True])
+    def initial_fault(self, request):
+        return request.param
+
+    @pytest.fixture()
+    def component(self, mock_capability_types, initial_power_mode, initial_fault):
+        return ReferenceSubarrayComponentManager._Component(
+            mock_capability_types, _power_mode=initial_power_mode, _faulty=initial_fault
+        )
+
+    @pytest.fixture()
+    def component_manager(
+        self,
+        mock_op_state_model,
+        mock_obs_state_model,
+        mock_capability_types,
+        logger,
+        component,
+    ):
+        """
+        Fixture that returns the component manager under test
+
+        :param mock_op_state_model: a mock state model for testing
+        :param logger: a logger for the component manager
+
+        :return: the component manager under test
+        """
+        return ReferenceSubarrayComponentManager(
+            mock_op_state_model,
+            mock_obs_state_model,
+            mock_capability_types,
+            logger=logger,
+            _component=component,
+        )
+
+    def test_state_changes_with_connect_and_disconnect(
+        self,
+        component,
+        component_manager,
+        mock_op_state_model,
+        mock_obs_state_model,
+        initial_power_mode,
+        initial_fault
+    ):
+        """
+        Test that the state model is updated with state changes when the
+        component manager connects to and disconnects from its
+        component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+        expected_action = (
+            "component_fault" if initial_fault else power_mode_map[initial_power_mode]
+        )
+
+        assert not component_manager.is_communicating
+
+        # While disconnected, tell the component to simulate an obsfault
+        # (but only if it is turned on and not faulty)
+        if initial_power_mode == PowerMode.ON and not initial_fault:
+            component.simulate_obsfault(True)
+
+            # The component is disconnected, so the state model cannot
+            # know that it has changed obs state
+            mock_obs_state_model.to_OBSFAULT.assert_not_called()
+
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+        assert mock_op_state_model.perform_action.call_args_list == [
+            (("component_unknown",),),
+            ((expected_action,),),
+        ]
+
+        if initial_power_mode == PowerMode.ON and not initial_fault:
+            # The component manager has noticed that it missed a change
+            # in the component while it was disconnected, and his
+            # updated the obs state model
+            mock_obs_state_model.to_OBSFAULT.assert_called_once_with()
+
+        mock_op_state_model.reset_mock()
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with(
+            "component_disconnected"
+        )
+
+    def test_simulate_communication_failure(self, component_manager, mock_op_state_model):
+        """
+        Test that we can simulate connection failure.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        """
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        mock_op_state_model.reset_mock()
+        component_manager.simulate_communication_failure(True)
+        assert not component_manager.is_communicating
+        mock_op_state_model.perform_action.assert_called_once_with("component_unknown")
+
+        with pytest.raises(ConnectionError, match="Failed to connect"):
+            component_manager.start_communicating()
+
+    @pytest.mark.parametrize("command", ["off", "standby", "on"])
+    def test_base_command_fails_when_disconnected(self, component_manager, command):
+        """
+        Test that component commands fail when the component manager
+        isn't connected to the component.
+
+        :param component_manager: the component manager under test
+        :param command: the command under test
+        """
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+        component_manager.start_communicating()
+        assert component_manager.is_communicating
+
+        component_manager.stop_communicating()
+        assert not component_manager.is_communicating
+        with pytest.raises(ConnectionError, match="Not connected"):
+            getattr(component_manager, command)()
+
+    @pytest.mark.parametrize(
+        ("command", "action"),
+        [
+            ("off", "component_off"),
+            ("standby", "component_standby"),
+            ("on", "component_on"),
+        ],
+    )
+    def test_base_command_succeeds_when_connected(
+        self,
+        component_manager,
+        mock_op_state_model,
+        initial_power_mode,
+        initial_fault,
+        command,
+        action,
+    ):
+        """
+        Test that component commands succeed when the component manager
+        is connected to the component.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param command: the name of the command under test
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component_manager, command)()
+
+            if power_mode_map[initial_power_mode] == action:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    @pytest.mark.parametrize(
+        ("call", "action"),
+        [
+            ("simulate_off", "component_off"),
+            ("simulate_standby", "component_standby"),
+            ("simulate_on", "component_on"),
+        ],
+    )
+    def test_simulated_change_propagates(
+        self,
+        component_manager,
+        mock_op_state_model,
+        component,
+        initial_power_mode,
+        initial_fault,
+        call,
+        action,
+    ):
+        """
+        Test that spontaneous changes to the state of the component
+        result in the correct action being performed on the state model.
+
+        :param component_manager: the component manager under test
+        :param mock_op_state_model: a mock state model for testing
+        :param state:
+        :param action: the action that is expected to be performed on
+            the state model
+        """
+        power_mode_map = {
+            PowerMode.OFF: "component_off",
+            PowerMode.STANDBY: "component_standby",
+            PowerMode.ON: "component_on",
+        }
+
+        component_manager.start_communicating()
+        mock_op_state_model.reset_mock()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            getattr(component, call)()
+            if action == power_mode_map[initial_power_mode]:
+                mock_op_state_model.perform_action.assert_not_called()
+            else:
+                mock_op_state_model.perform_action.assert_called_once_with(action)
+
+    def test_reset_from_fault(
+        self, component_manager, mock_op_state_model, initial_fault
+    ):
+        component_manager.start_communicating()
+        assert component_manager.faulty == initial_fault
+        mock_op_state_model.reset_mock()
+
+        component_manager.reset()
+        assert not component_manager.faulty
+        mock_op_state_model.perform_action.assert_not_called()
+
+    def test_assign(
+        self,
+        component_manager,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource_1 = mock_resource_factory()
+        mock_resource_2 = mock_resource_factory()
+
+        component_manager.assign([mock_resource_1])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_resourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.assign([mock_resource_2])
+        mock_obs_state_model.perform_action.assert_not_called()
+
+        component_manager.release([mock_resource_1])
+        mock_obs_state_model.perform_action.assert_not_called()
+
+        component_manager.release([mock_resource_2])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_unresourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.assign([mock_resource_1, mock_resource_2])
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_resourced"
+        )
+        mock_obs_state_model.reset_mock()
+
+        component_manager.release_all()
+        mock_obs_state_model.perform_action.assert_called_once_with(
+            "component_unresourced"
+        )
+
+    def test_configure(
+        self,
+        component_manager,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration_1 = mock_config_factory()
+        mock_configuration_2 = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_resourced"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.configure(mock_configuration_1)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_configured"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.configure(mock_configuration_2)
+            mock_obs_state_model.perform_action.assert_not_called()
+
+            component_manager.deconfigure()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_unconfigured"
+            )
+            mock_obs_state_model.reset_mock()
+
+    def test_scan(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+        mock_scan_args,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            component_manager.configure(mock_configuration)
+            mock_obs_state_model.reset_mock()
+
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.end_scan()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_not_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_scan_stopped()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_not_scanning"
+            )
+            mock_obs_state_model.reset_mock()
+
+    def test_obsfault_reset_restart(
+        self,
+        component_manager,
+        component,
+        initial_power_mode,
+        initial_fault,
+        mock_obs_state_model,
+        mock_resource_factory,
+        mock_config_factory,
+        mock_scan_args,
+    ):
+        component_manager.start_communicating()
+
+        mock_resource = mock_resource_factory()
+        mock_configuration = mock_config_factory()
+
+        raise_context = (
+            pytest.raises(ComponentFault, match="")
+            if initial_fault
+            else pytest.raises(ComponentError, match="Component is not ON")
+            if initial_power_mode != PowerMode.ON
+            else contextlib.nullcontext()
+        )
+
+        with raise_context:
+            component_manager.assign([mock_resource])
+            component_manager.configure(mock_configuration)
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(True)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_obsfault"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(False)
+            component_manager.obsreset()
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_unconfigured"
+            )
+
+            component_manager.configure(mock_configuration)
+            component_manager.scan(mock_scan_args)
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(True)
+            mock_obs_state_model.perform_action.assert_called_once_with(
+                "component_obsfault"
+            )
+            mock_obs_state_model.reset_mock()
+
+            component.simulate_obsfault(False)
+            component_manager.restart()
+            assert mock_obs_state_model.perform_action.call_args_list == [
+                (("component_unconfigured",),),
+                (("component_unresourced",),),
+            ]
diff --git a/tests/test_subarray_device.py b/tests/test_subarray_device.py
index 8a1c49aaa5c0c5c1dc6652aa012a519fb43d2224..95a9c3c4589b1f71a384d1ed789630d6832a2582 100644
--- a/tests/test_subarray_device.py
+++ b/tests/test_subarray_device.py
@@ -8,14 +8,15 @@
 #########################################################################################
 """Contain the tests for the SKASubarray."""
 
-import logging
+import json
 import re
 import pytest
 
 from tango import DevState, DevFailed
 
 # PROTECTED REGION ID(SKASubarray.test_additional_imports) ENABLED START #
-from ska_tango_base import SKASubarray, SKASubarrayResourceManager, SKASubarrayStateModel
+from ska_tango_base import SKASubarray
+from ska_tango_base.base import OpStateModel
 from ska_tango_base.commands import ResultCode
 from ska_tango_base.control_model import (
     AdminMode,
@@ -27,70 +28,52 @@ from ska_tango_base.control_model import (
     TestMode,
 )
 from ska_tango_base.faults import CommandError
-
-from .conftest import load_state_machine_spec, ModelStateMachineTester
-
+from ska_tango_base.subarray import (
+    ReferenceSubarrayComponentManager, SubarrayObsStateModel
+)
 # PROTECTED REGION END #    //  SKASubarray.test_additional_imports
 
 
-@pytest.fixture
-def subarray_state_model():
-    """
-    Yields a new SKASubarrayStateModel for testing
-    """
-    yield SKASubarrayStateModel(logging.getLogger())
-
-
-@pytest.mark.state_machine_tester(load_state_machine_spec("subarray_state_machine"))
-class TestSKASubarrayStateModel(ModelStateMachineTester):
+class TestSKASubarray:
     """
-    This class contains the test for the SKASubarrayStateModel class.
+    Test cases for SKASubarray device.
     """
 
-    @pytest.fixture
-    def machine(self, subarray_state_model):
+    @pytest.fixture(scope="class")
+    def device_properties(self):
         """
-        Fixture that returns the state machine under test in this class
-        """
-        yield subarray_state_model
-
-    def assert_state(self, machine, state):
+        Fixture that returns device_properties to be provided to the
+        device under test.
         """
-        Assert the current state of this state machine, based on the
-        values of the adminMode, opState and obsState attributes of this
-        model.
-
-        :param machine: the state machine under test
-        :type machine: state machine object instance
-        :param state: the state that we are asserting to be the current
-            state of the state machine under test
-        :type state: str
+        return {
+            "CapabilityTypes": ["BAND1", "BAND2"],
+            'LoggingTargetsDefault': '',
+            'GroupDefinitions': '',
+            'SkaLevel': '4',
+            'SubID': '1',
+        }
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
         """
-        assert machine.admin_mode == state["admin_mode"]
-        assert machine.op_state == state["op_state"]
-        assert machine.obs_state == state["obs_state"]
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
 
-
-class TestSKASubarray:
-    """
-    Test cases for SKASubarray device.
-    """
-
-    properties = {
-        'CapabilityTypes': '',
-        'GroupDefinitions': '',
-        'SkaLevel': '4',
-        'LoggingTargetsDefault': '',
-        'SubID': '',
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKASubarray.numpy = MagicMock()
-        # PROTECTED REGION ID(SKASubarray.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKASubarray.test_mocking
+        This implementation provides a concrete subclass of the device
+        class under test, some properties, and a memorized value for
+        adminMode.
+        """
+        return {
+            "device": SKASubarray,
+            "component_manager_patch": lambda self: ReferenceSubarrayComponentManager(
+                self.op_state_model,
+                self.obs_state_model,
+                self.CapabilityTypes,
+                logger=self.logger,
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
 
     @pytest.mark.skip(reason="Not implemented")
     def test_properties(self, tango_context):
@@ -106,7 +89,7 @@ class TestSKASubarray:
         # PROTECTED REGION ID(SKASubarray.test_Abort) ENABLED START #
 
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
         tango_context.device.Configure('{"BAND1": 2}')
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
@@ -126,7 +109,7 @@ class TestSKASubarray:
         """Test for Configure"""
         # PROTECTED REGION ID(SKASubarray.test_Configure) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         obs_state_callback.assert_call(ObsState.IDLE)
@@ -146,8 +129,9 @@ class TestSKASubarray:
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKASubarray.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKASubarray, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKASubarray.test_GetVersionInfo
@@ -178,13 +162,14 @@ class TestSKASubarray:
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         obs_state_callback.assert_call(ObsState.EMPTY)
 
-        tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}')
+        resources_to_assign = ["BAND1", "BAND2"]
+        tango_context.device.AssignResources(json.dumps(resources_to_assign))
 
         obs_state_callback.assert_calls(
             [ObsState.RESOURCING, ObsState.IDLE]
         )
         assert tango_context.device.ObsState == ObsState.IDLE
-        assert tango_context.device.assignedResources == ('BAND1', 'BAND2')
+        assert list(tango_context.device.assignedResources) == resources_to_assign
 
         tango_context.device.ReleaseAllResources()
         obs_state_callback.assert_calls(
@@ -202,7 +187,7 @@ class TestSKASubarray:
         """Test for EndSB"""
         # PROTECTED REGION ID(SKASubarray.test_EndSB) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
         tango_context.device.Configure('{"BAND1": 2}')
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
@@ -221,7 +206,7 @@ class TestSKASubarray:
         """Test for EndScan"""
         # PROTECTED REGION ID(SKASubarray.test_EndScan) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
         tango_context.device.Configure('{"BAND1": 2}')
         tango_context.device.Scan('{"id": 123}')
 
@@ -243,7 +228,7 @@ class TestSKASubarray:
         # PROTECTED REGION ID(SKASubarray.test_ReleaseAllResources) ENABLED START #
         # assert tango_context.device.ReleaseAllResources() == [""]
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1", "BAND2"]))
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         obs_state_callback.assert_call(ObsState.IDLE)
@@ -262,12 +247,12 @@ class TestSKASubarray:
         """Test for ReleaseResources"""
         # PROTECTED REGION ID(SKASubarray.test_ReleaseResources) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1", "BAND2"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1", "BAND2"]))
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         obs_state_callback.assert_call(ObsState.IDLE)
 
-        tango_context.device.ReleaseResources('{"example": ["BAND1"]}')
+        tango_context.device.ReleaseResources(json.dumps(["BAND1"]))
 
         obs_state_callback.assert_calls(
             [ObsState.RESOURCING, ObsState.IDLE]
@@ -282,7 +267,7 @@ class TestSKASubarray:
         """Test for Reset"""
         # PROTECTED REGION ID(SKASubarray.test_Reset) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
         tango_context.device.Configure('{"BAND1": 2}')
         tango_context.device.Abort()
 
@@ -305,14 +290,14 @@ class TestSKASubarray:
         """Test for Scan"""
         # PROTECTED REGION ID(SKASubarray.test_Scan) ENABLED START #
         tango_context.device.On()
-        tango_context.device.AssignResources('{"example": ["BAND1"]}')
+        tango_context.device.AssignResources(json.dumps(["BAND1"]))
         tango_context.device.Configure('{"BAND1": 2}')
 
         obs_state_callback = tango_change_event_helper.subscribe("obsState")
         obs_state_callback.assert_call(ObsState.READY)
 
         assert tango_context.device.Scan('{"id": 123}') == [
-            [ResultCode.STARTED], ["Scan command STARTED - config {'id': 123}"]
+            [ResultCode.STARTED], ["Scan command started"]
         ]
 
         obs_state_callback.assert_call(ObsState.SCANNING)
@@ -336,21 +321,26 @@ class TestSKASubarray:
     def test_adminMode(self, tango_context, tango_change_event_helper):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKASubarray.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
-
-        tango_context.device.Disable()
-        assert tango_context.device.state() == DevState.DISABLE
+        device_under_test = tango_context.device
+        assert device_under_test.state() == DevState.OFF
+        assert device_under_test.adminMode == AdminMode.ONLINE
 
         admin_mode_callback = tango_change_event_helper.subscribe("adminMode")
-        dev_state_callback = tango_change_event_helper.subscribe("state")
-        admin_mode_callback.assert_call(AdminMode.MAINTENANCE)
-        dev_state_callback.assert_call(DevState.DISABLE)
+        op_state_callback = tango_change_event_helper.subscribe("state")
+        admin_mode_callback.assert_call(AdminMode.ONLINE)
+        op_state_callback.assert_call(DevState.OFF)
 
         tango_context.device.adminMode = AdminMode.OFFLINE
-        assert tango_context.device.adminMode == AdminMode.OFFLINE
-        assert tango_context.device.state() == DevState.DISABLE
+        assert device_under_test.state() == DevState.DISABLE
+        assert device_under_test.adminMode == AdminMode.OFFLINE
         admin_mode_callback.assert_call(AdminMode.OFFLINE)
+        op_state_callback.assert_call(DevState.DISABLE)
 
+        tango_context.device.adminMode = AdminMode.MAINTENANCE
+        assert device_under_test.state() == DevState.OFF
+        assert device_under_test.adminMode == AdminMode.MAINTENANCE
+        admin_mode_callback.assert_call(AdminMode.MAINTENANCE)
+        op_state_callback.assert_calls([DevState.UNKNOWN, DevState.OFF])
         # PROTECTED REGION END #    //  SKASubarray.test_adminMode
 
     # PROTECTED REGION ID(SKASubarray.test_buildState_decorators) ENABLED START #
@@ -442,6 +432,7 @@ class TestSKASubarray:
     def test_assignedResources(self, tango_context):
         """Test for assignedResources"""
         # PROTECTED REGION ID(SKASubarray.test_assignedResources) ENABLED START #
+        tango_context.device.On()
         assert tango_context.device.assignedResources is None
         # PROTECTED REGION END #    //  SKASubarray.test_assignedResources
 
@@ -450,148 +441,96 @@ class TestSKASubarray:
     def test_configuredCapabilities(self, tango_context):
         """Test for configuredCapabilities"""
         # PROTECTED REGION ID(SKASubarray.test_configuredCapabilities) ENABLED START #
+        tango_context.device.On()
         assert tango_context.device.configuredCapabilities == ("BAND1:0", "BAND2:0")
         # PROTECTED REGION END #    //  SKASubarray.test_configuredCapabilities
 
 
-@pytest.fixture
-def resource_manager():
-    """
-    Fixture that yields an SKASubarrayResourceManager
-
-    :yields: a SKASubarrayResourceManager instance
-    """
-    yield SKASubarrayResourceManager()
-
-
-class TestSKASubarrayResourceManager:
+class TestSKASubarray_commands:
     """
-    Test suite for SKASubarrayResourceManager class
+    This class contains tests of SKASubarray commands
     """
-
-    def test_ResourceManager_assign(self, resource_manager):
+    @pytest.fixture
+    def op_state_model(self, logger):
         """
-        Test that the ResourceManager assigns resource correctly.
+        Yields a new OpStateModel for testing
         """
-        # create a resource manager and check that it is empty
-        assert not len(resource_manager)
-        assert resource_manager.get() == set()
-
-        resource_manager.assign('{"example": ["A"]}')
-        assert len(resource_manager) == 1
-        assert resource_manager.get() == set(["A"])
-
-        resource_manager.assign('{"example": ["A"]}')
-        assert len(resource_manager) == 1
-        assert resource_manager.get() == set(["A"])
+        yield OpStateModel(logger)
 
-        resource_manager.assign('{"example": ["A", "B"]}')
-        assert len(resource_manager) == 2
-        assert resource_manager.get() == set(["A", "B"])
-
-        resource_manager.assign('{"example": ["A"]}')
-        assert len(resource_manager) == 2
-        assert resource_manager.get() == set(["A", "B"])
-
-        resource_manager.assign('{"example": ["A", "C"]}')
-        assert len(resource_manager) == 3
-        assert resource_manager.get() == set(["A", "B", "C"])
-
-        resource_manager.assign('{"example": ["D"]}')
-        assert len(resource_manager) == 4
-        assert resource_manager.get() == set(["A", "B", "C", "D"])
-
-    def test_ResourceManager_release(self, resource_manager):
+    @pytest.fixture
+    def subarray_state_model(self, logger):
         """
-        Test that the ResourceManager releases resource correctly.
+        Yields a new SubarrayObsStateModel for testing
         """
-        resource_manager.assign('{"example": ["A", "B", "C", "D"]}')
-
-        # okay to release resources not assigned; does nothing
-        resource_manager.release('{"example": ["E"]}')
-        assert len(resource_manager) == 4
-        assert resource_manager.get() == set(["A", "B", "C", "D"])
-
-        # check release does what it should
-        resource_manager.release('{"example": ["D"]}')
-        assert len(resource_manager) == 3
-        assert resource_manager.get() == set(["A", "B", "C"])
+        yield SubarrayObsStateModel(logger)
 
-        # okay to release resources both assigned and not assigned
-        resource_manager.release('{"example": ["C", "D"]}')
-        assert len(resource_manager) == 2
-        assert resource_manager.get() == set(["A", "B"])
-
-        # check release all does what it should
-        resource_manager.release_all()
-        assert len(resource_manager) == 0
-        assert resource_manager.get() == set()
-
-        # okay to call release_all when already empty
-        resource_manager.release_all()
-        assert len(resource_manager) == 0
-        assert resource_manager.get() == set()
+    @pytest.fixture()
+    def component_manager(self, op_state_model, subarray_state_model, logger, mocker):
+        """
+        Fixture that returns the component manager under test
 
+        :param mock_op_state_model: a mock state model for testing
+        :param logger: a logger for the component manager
 
-class TestSKASubarray_commands:
-    """
-    This class contains tests of SKASubarray commands
-    """
+        :return: the component manager under test
+        """
+        mock_capability_types = ["foo", "bah"]
+        return ReferenceSubarrayComponentManager(
+            op_state_model,
+            subarray_state_model,
+            mock_capability_types,
+            logger=logger
+        )
 
-    def test_AssignCommand(self, resource_manager, subarray_state_model):
+    def test_AssignCommand(self, component_manager, op_state_model, subarray_state_model):
         """
         Test for SKASubarray.AssignResourcesCommand
         """
+        op_state_model._straight_to_state("DISABLE")
+        component_manager.start_communicating()
+        component_manager.on()
+
         assign_resources = SKASubarray.AssignResourcesCommand(
-            resource_manager,
-            subarray_state_model
+            component_manager,
+            op_state_model,
+            subarray_state_model,
         )
-
-        machine_spec = load_state_machine_spec("subarray_state_machine")
-        states = machine_spec["states"]
-        # in all states except EMPTY and IDLE, the assign resources command is
-        # not permitted, should not be allowed, should fail, should have no
-        # side-effect
-        for state in set(states) - {
-            "EMPTY_ONLINE",
-            "EMPTY_MAINTENANCE",
-            "IDLE_ONLINE",
-            "IDLE_MAINTENANCE",
-        }:
-
-            subarray_state_model._straight_to_state(**states[state])
+        for obs_state_name in [
+            "RESOURCING_EMPTY",
+            "RESOURCING_IDLE",
+            "CONFIGURING_IDLE",
+            "CONFIGURING_READY",
+            "READY",
+            "SCANNING",
+            "ABORTING",
+            "ABORTED",
+            "RESETTING",
+            "RESTARTING",
+            "FAULT",
+        ]:
+            subarray_state_model._straight_to_state(obs_state_name)
+            prior_obs_state = subarray_state_model.obs_state
             assert not assign_resources.is_allowed()
             with pytest.raises(CommandError):
-                assign_resources('{"example": ["foo"]}')
-            assert not len(resource_manager)
-            assert resource_manager.get() == set()
-            assert subarray_state_model.admin_mode == states[state]["admin_mode"]
-            assert subarray_state_model.op_state == states[state]["op_state"]
-            assert subarray_state_model.obs_state == states[state]["obs_state"]
-
-        # now push to empty, a state in which is IS allowed
-        subarray_state_model._straight_to_state(**states["EMPTY_ONLINE"])
-        assert assign_resources.is_allowed()
-        assert assign_resources('{"example": ["foo"]}') == (
+                assign_resources(["foo"])
+            assert component_manager.assigned_resources == []
+            assert subarray_state_model.obs_state == prior_obs_state
+
+        # now push to empty, a state in which the command IS allowed
+        subarray_state_model._straight_to_state("EMPTY")
+        assert assign_resources.is_allowed(True)
+        assert assign_resources(["foo"]) == (
             ResultCode.OK,
             "AssignResources command completed OK",
         )
-        assert len(resource_manager) == 1
-        assert resource_manager.get() == set(["foo"])
-
-        assert subarray_state_model.admin_mode == states["IDLE_ONLINE"]["admin_mode"]
-        assert subarray_state_model.op_state == states["IDLE_ONLINE"]["op_state"]
-        assert subarray_state_model.obs_state == states["IDLE_ONLINE"]["obs_state"]
+        assert component_manager.assigned_resources == ["foo"]
+        assert subarray_state_model.obs_state == ObsState.IDLE
 
         # AssignResources is still allowed in IDLE
         assert assign_resources.is_allowed()
-        assert assign_resources('{"example": ["bar"]}') == (
+        assert assign_resources(["bar"]) == (
             ResultCode.OK,
             "AssignResources command completed OK",
         )
-        assert len(resource_manager) == 2
-        assert resource_manager.get() == set(["foo", "bar"])
-
-        assert subarray_state_model.admin_mode == states["IDLE_ONLINE"]["admin_mode"]
-        assert subarray_state_model.op_state == states["IDLE_ONLINE"]["op_state"]
-        assert subarray_state_model.obs_state == states["IDLE_ONLINE"]["obs_state"]
+        assert component_manager.assigned_resources == ["bar", "foo"]
+        assert subarray_state_model.obs_state == ObsState.IDLE
diff --git a/tests/test_tel_state_device.py b/tests/test_tel_state_device.py
index 6f3eb7d8d559318cc6e7230d5ddc24424c1230e5..d682835c39a2680fc39fa10ee8b2b80961c2f408 100644
--- a/tests/test_tel_state_device.py
+++ b/tests/test_tel_state_device.py
@@ -13,6 +13,8 @@ import pytest
 from tango import DevState
 
 # PROTECTED REGION ID(SKATelState.test_additional_imports) ENABLED START #
+from ska_tango_base import SKATelState
+from ska_tango_base.base import ReferenceBaseComponentManager
 from ska_tango_base.control_model import AdminMode, ControlMode, HealthState, SimulationMode, TestMode
 # PROTECTED REGION END #    //  SKATelState.test_additional_imports
 
@@ -21,23 +23,27 @@ from ska_tango_base.control_model import AdminMode, ControlMode, HealthState, Si
 @pytest.mark.usefixtures("tango_context", "initialize_device")
 # PROTECTED REGION END #    //  SKATelState.test_SKATelState_decorators
 class TestSKATelState(object):
-    """Test case for packet generation."""
-
-    properties = {
-        'TelStateConfigFile': '',
-        'SkaLevel': '4',
-        'GroupDefinitions': '',
-        'LoggingTargetsDefault': '',
-    }
-
-    @classmethod
-    def mocking(cls):
-        """Mock external libraries."""
-        # Example : Mock numpy
-        # cls.numpy = SKATelState.numpy = MagicMock()
-        # PROTECTED REGION ID(SKATelState.test_mocking) ENABLED START #
-        # PROTECTED REGION END #    //  SKATelState.test_mocking
-
+    """
+    Test class for tests of the SKATelState device class.
+    """
+
+    @pytest.fixture(scope="class")
+    def device_test_config(self, device_properties):
+        """
+        Fixture that specifies the device to be tested, along with its
+        properties and memorized attributes.
+        """
+        return {
+            "device": SKATelState,
+            "component_manager_patch": lambda self: ReferenceBaseComponentManager(
+                self.op_state_model, logger=self.logger
+            ),
+            "properties": device_properties,
+            "memorized": {"adminMode": str(AdminMode.ONLINE.value)},
+        }
+
+
+    @pytest.mark.skip("Not implemented")
     def test_properties(self, tango_context):
         # Test the properties
         # PROTECTED REGION ID(SKATelState.test_properties) ENABLED START #
@@ -66,8 +72,9 @@ class TestSKATelState(object):
         """Test for GetVersionInfo"""
         # PROTECTED REGION ID(SKATelState.test_GetVersionInfo) ENABLED START #
         versionPattern = re.compile(
-            r'SKATelState, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
-            r'A set of generic base devices for SKA Telescope.')
+            f'{tango_context.device.info().dev_class}, ska_tango_base, [0-9]+.[0-9]+.[0-9]+, '
+            'A set of generic base devices for SKA Telescope.'
+        )
         versionInfo = tango_context.device.GetVersionInfo()
         assert (re.match(versionPattern, versionInfo[0])) is not None
         # PROTECTED REGION END #    //  SKATelState.test_GetVersionInfo
@@ -105,7 +112,7 @@ class TestSKATelState(object):
     def test_adminMode(self, tango_context):
         """Test for adminMode"""
         # PROTECTED REGION ID(SKATelState.test_adminMode) ENABLED START #
-        assert tango_context.device.adminMode == AdminMode.MAINTENANCE
+        assert tango_context.device.adminMode == AdminMode.ONLINE
         # PROTECTED REGION END #    //  SKATelState.test_adminMode
 
     # PROTECTED REGION ID(SKATelState.test_controlMode_decorators) ENABLED START #