diff --git a/tangostationcontrol/tangostationcontrol/clients/README.md b/tangostationcontrol/tangostationcontrol/clients/README.md
index c7085de860b20ade0e8d759f3bbd72d211e452df..9a0c749575c32ad6d582427f154562d97a7fc8a6 100644
--- a/tangostationcontrol/tangostationcontrol/clients/README.md
+++ b/tangostationcontrol/tangostationcontrol/clients/README.md
@@ -16,11 +16,8 @@ Inside lofar/tango/tangostationcontrol/tangostationcontrol/devices/lofar_device.
 	Init_value:		Initialisation value. If none is presents, fills the attribute with zero  data of the correct type and dimension
 	**kwargs: any other non attribute_wrapper arguments.
 NOTE: the `__init__` function contains wrappers for the unassigned read/write functions. In previous versions the read function of an RW attribute used to return the last value it had written *to* the client  instead of the value from the client. This has since been changed.  
-
-`initial_value`:
-	This function fills the attribute with a default value of all zero's with the proper dimensions and type if None is specified. 
 	
-`Set_comm_client`:
+`set_comm_client`:
 This function can be called to assign a read and write function to the attribute using the data accessor or client given to this function. The attribute wrapper assumes the client is running and has a function called ‘setup_attribute’ which will provide it with a valid read/write function. 
 
 `async_set_comm_client`:
@@ -29,14 +26,6 @@ This function can be called to assign a read and write function to the attribute
 `set_pass_func`:
 	Can be called to assign a 'fake' read/write function. This is useful as a fallback option while development is still ongoing.
 
-`_decorate_read_function`:
-Wrap an attribute read function to annotate its exceptions with our comms_annotation to be able to identify which attribute triggered the error.
-
-
-
-`_decorate_write_function`: 
-Wrap an attribute write function to annotate its exceptions with our comms_annotation to be able to identify which attribute triggered the error.
-
 
 
 
diff --git a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
index 5d9f5bdc8cd123e0ffd28c962548fc94bdf82572..9375b6d5d2b75c5ebfd66b3cab2318e586227b65 100644
--- a/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
+++ b/tangostationcontrol/tangostationcontrol/clients/attribute_wrapper.py
@@ -1,6 +1,5 @@
 from tango.server import attribute
 from tango import AttrWriteType
-import numpy
 
 from tangostationcontrol.devices.device_decorators import fault_on_error
 import logging
@@ -8,12 +7,46 @@ import logging
 logger = logging.getLogger()
 
 
+class attribute_io(object):
+    """ Holds the I/O functionality for an attribute for a specific device. """
+
+    def __init__(self, device, attribute_wrapper):
+        # Link to the associated device
+        self.device = device
+
+        # Link to the associated attribute wrapper
+        self.attribute_wrapper = attribute_wrapper
+
+        # Link to last (written) value
+        self.cached_value = None
+
+        # Specific read and write functions for this attribute on this device
+        self.read_function = lambda: None
+        self.write_function = lambda value: None
+
+    def cached_read_function(self):
+        """ Return the last (written) value, if available. Otherwise, read
+            from the device. """
+
+        if self.cached_value is not None:
+            return self.cached_value
+
+        self.cached_value = self.read_function()
+        return self.cached_value
+
+    def cached_write_function(self, value):
+        """ Writes the given value to the device, and updates the cache. """
+
+        self.write_function(value)
+        self.cached_value = value
+
+
 class attribute_wrapper(attribute):
     """
     Wraps all the attributes in a wrapper class to manage most of the redundant code behind the scenes
     """
 
-    def __init__(self, comms_id=None, comms_annotation=None, datatype=None, dims=(1,), access=AttrWriteType.READ, init_value=None, **kwargs):
+    def __init__(self, comms_id=None, comms_annotation=None, datatype=None, dims=(1,), access=AttrWriteType.READ, **kwargs):
         """
         wraps around the tango Attribute class. Provides an easier interface for 1d or 2d arrays. Also provides a way to abstract
         managing the communications interface.
@@ -34,43 +67,43 @@ class attribute_wrapper(attribute):
         self.comms_id = comms_id # store data that can be used to identify the comms interface to use. not used by the wrapper itself
         self.comms_annotation = comms_annotation  # store data that can be used by the comms interface. not used by the wrapper itself
 
-        self.init_value = init_value
-        is_scalar = dims == (1,)
-
-        self.numpy_type = datatype  # tango changes our attribute to their representation (E.g numpy.int64 becomes "DevLong64")
+        self.datatype = datatype
 
-        # check if not scalar
-        if is_scalar:
-            # scalar, just set the single dimension.
+        if dims == (1,):
             # Tango defines a scalar as having dimensions (1,0), see https://pytango.readthedocs.io/en/stable/server_api/attribute.html
             max_dim_x = 1
             max_dim_y = 0
-        else:
-            # get first dimension
+            dtype = datatype
+        elif len(dims) == 1:
             max_dim_x = dims[0]
-
-            # single dimension/spectrum requires the datatype to be wrapped in a tuple
-            datatype = (datatype,)
-
-            if len(dims) == 2:
-                # get second dimension
-                max_dim_y = dims[1]
-                # wrap the datatype tuple in another tuple for 2d arrays/images
-                datatype = (datatype,)
-            else:
-                max_dim_y = 0
+            max_dim_y = 0
+            dtype = (datatype,)
+        elif len(dims) == 2:
+            max_dim_x = dims[0]
+            max_dim_y = dims[1]
+            dtype = ((datatype,),)
+        else:
+            raise ValueError(f"Only up to 2D arrays are supported. Supplied dimensions: {dims}")
 
         if access == AttrWriteType.READ_WRITE:
             """ If the attribute is of READ_WRITE type, assign the write and read functions to it"""
 
+            # we return the last written value, as we are the only ones in control,
+            # and the hardware does not necessarily return what we've written
+            # (see L2SDP-725).
+
             @fault_on_error()
             def write_func_wrapper(device, value):
                 """
                 write_func_wrapper writes a value to this attribute
                 """
 
-                self.write_function(value)
-                device.value_dict[self] = value
+                try:
+                    io = self.get_attribute_io(device)
+
+                    return io.cached_write_function(value)
+                except Exception as e:
+                    raise e.__class__(f"Could not write attribute {comms_annotation}") from e
 
             @fault_on_error()
             def read_func_wrapper(device):
@@ -78,16 +111,9 @@ class attribute_wrapper(attribute):
                 read_func_wrapper reads the attribute value, stores it and returns it"
                 """
                 try:
-                    # we return the last written value, as we are the only ones in control,
-                    # and the hardware does not necessarily return what we've written
-                    # (see L2SDP-725).
-                    if self in device.value_dict:
-                        return device.value_dict[self]
-
-                    # value was never written, so obtain current one and cache that instead
-                    value = self.read_function()
-                    device.value_dict[self] = value
-                    return value
+                    io = self.get_attribute_io(device)
+
+                    return io.cached_read_function()
                 except Exception as e:
                     raise e.__class__(f"Could not read attribute {comms_annotation}") from e
 
@@ -102,7 +128,9 @@ class attribute_wrapper(attribute):
                 read_func_wrapper reads the attribute value, stores it and returns it"
                 """
                 try:
-                    return self.read_function()
+                    io = self.get_attribute_io(device)
+
+                    return io.read_function()
                 except Exception as e:
                     raise e.__class__(f"Could not read attribute {comms_annotation}") from e
 
@@ -114,60 +142,20 @@ class attribute_wrapper(attribute):
         #
         # NOTE: fisallowed=<callable> does not work: https://gitlab.com/tango-controls/pytango/-/issues/435
         # So we have to use fisallowed=<str> here, which causes the function device.<str> to be called.
-        super().__init__(dtype=datatype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_access_allowed", **kwargs)
+        super().__init__(dtype=dtype, max_dim_y=max_dim_y, max_dim_x=max_dim_x, access=access, fisallowed="is_attribute_access_allowed", **kwargs)
 
-    def initial_value(self):
+    def get_attribute_io(self, device):
         """
-        returns a numpy array filled with zeroes fit to the size of the attribute. Or if init_value is not the default None, return that value
+        returns the attribute I/O functions from a certain device, or registers it if not present
         """
-        if self.init_value is not None:
-            return self.init_value
-
-        if self.dim_y > 1:
-            dims = (self.dim_x, self.dim_y)
-        else:
-            dims = (self.dim_x,)
-
-        # x and y are swapped for numpy and Tango. to maintain tango conventions, x and y are swapped for numpy
-        if len(dims) == 2:
-            numpy_dims = tuple((dims[1], dims[0]))
-        else:
-            numpy_dims = dims
-
-        if self.dim_x == 1:
 
-            if self.numpy_type == str:
-                value = ''
-            else:
-                value = self.numpy_type(0)
-        else:
-            value = numpy.zeros(numpy_dims, dtype=self.numpy_type)
-
-        return value
-
-    def _decorate_read_function(self, read_attr_func):
-        """ Wrap an attribute read function to annotate its exceptions with our
-            comms_annotation to be able to identify which attribute triggered the error. """
-        def wrapper():
-            try:
-                return read_attr_func()
-            except Exception as e:
-                raise Exception(f"Failed to read attribute {self.comms_annotation}") from e
-
-        return wrapper
-
-    def _decorate_write_function(self, write_attr_func):
-        """ Wrap an attribute write function to annotate its exceptions with our
-            comms_annotation to be able to identify which attribute triggered the error. """
-        def wrapper(value):
-            try:
-                write_attr_func(value)
-            except Exception as e:
-                raise Exception(f"Failed to write attribute {self.comms_annotation}") from e
-
-        return wrapper
+        try:
+            return device._attribute_wrapper_io[self]
+        except KeyError:
+            device._attribute_wrapper_io[self] = attribute_io(device, self)
+            return device._attribute_wrapper_io[self]
 
-    def set_comm_client(self, client):
+    def set_comm_client(self, device, client):
         """
         takes a communications client as input arguments This client should be of a class containing a "get_mapping" function
         and return a read and write function that the wrapper will use to get/set data.
@@ -175,29 +163,28 @@ class attribute_wrapper(attribute):
         try:
             read_attr_func, write_attr_func = client.setup_attribute(self.comms_annotation, self)
 
-            self.read_function  = self._decorate_read_function(read_attr_func)
-            self.write_function = self._decorate_write_function(write_attr_func)
+            io = self.get_attribute_io(device)
+            io.read_function  = read_attr_func
+            io.write_function = write_attr_func
         except Exception as e:
             raise Exception(f"Exception while setting {client.__class__.__name__} attribute with annotation: '{self.comms_annotation}'") from e
 
-    async def async_set_comm_client(self, client):
+    async def async_set_comm_client(self, device, client):
         """
           Asynchronous version of set_comm_client.
         """
         try:
             read_attr_func, write_attr_func = await client.setup_attribute(self.comms_annotation, self)
 
-            self.read_function  = self._decorate_read_function(read_attr_func)
-            self.write_function = self._decorate_write_function(write_attr_func)
+            io = self.get_attribute_io(device)
+            io.read_function  = read_attr_func
+            io.write_function = write_attr_func
         except Exception as e:
             raise Exception(f"Exception while setting {client.__class__.__name__} attribute with annotation: '{self.comms_annotation}'") from e
 
-
-    def set_pass_func(self):
-        def pass_func(value=None):
-            pass
-
+    def set_pass_func(self, device):
         logger.debug("using pass function for attribute with annotation: {}".format(self.comms_annotation))
 
-        self.read_function = pass_func
-        self.write_function = pass_func
+        io = self.get_attribute_io(device)
+        io.read_function  = lambda: None
+        io.write_function = lambda value: None
diff --git a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py
index 448b4fc5f1754cb54273140aa240256f4d6ff58d..53211e52fea7472390d82c450543eb8351ca165f 100644
--- a/tangostationcontrol/tangostationcontrol/clients/opcua_client.py
+++ b/tangostationcontrol/tangostationcontrol/clients/opcua_client.py
@@ -175,7 +175,7 @@ class OPCUAConnection(AsyncCommClient):
         # get all the necessary data to set up the read/write functions from the attribute_wrapper
         dim_x = attribute.dim_x
         dim_y = attribute.dim_y
-        ua_type = numpy_to_OPCua_dict[attribute.numpy_type]	 # convert the numpy type to a corresponding UA type
+        ua_type = numpy_to_OPCua_dict[attribute.datatype] # convert the numpy type to a corresponding UA type
 
         # configure and return the read/write functions
         prot_attr = ProtocolAttribute(node, dim_x, dim_y, ua_type)
diff --git a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py
index 4776bd15f922714739c05f8fbb316606e923877f..f75b6bcf4c4f8a7fd26fd85b6d0a4973a4d8d0dc 100644
--- a/tangostationcontrol/tangostationcontrol/clients/snmp_client.py
+++ b/tangostationcontrol/tangostationcontrol/clients/snmp_client.py
@@ -94,7 +94,7 @@ class SNMP_client(CommClient):
 
         dim_x = attribute.dim_x
         dim_y = attribute.dim_y
-        dtype = attribute.numpy_type
+        dtype = attribute.datatype
 
         return dim_x, dim_y, dtype
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/docker_device.py b/tangostationcontrol/tangostationcontrol/devices/docker_device.py
index 5066e9305014bb6ce3e98ba69491c01e5e6e8821..b98bf53e95c65a12c5cdbc224f43d4acd674f4de 100644
--- a/tangostationcontrol/tangostationcontrol/devices/docker_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/docker_device.py
@@ -116,7 +116,7 @@ class Docker(lofar_device):
     async def _connect_docker(self):
         # tie attributes to client
         for i in self.attr_list():
-            await i.async_set_comm_client(self.docker_client)
+            await i.async_set_comm_client(self, self.docker_client)
 
         await self.docker_client.start()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
index a005ab7aca27f1288b514ca1e677fa64a1bd868b..cba1a6578894cf88de1a6e6fad8b308515187ea1 100644
--- a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/lofar_device.py
@@ -88,10 +88,10 @@ class lofar_device(Device, metaclass=DeviceMeta):
         """ Return a list of all the attribute_wrapper members of this class. """
         return [v for k, v in cls.__dict__.items() if type(v) == attribute_wrapper]
 
-    def setup_value_dict(self):
-        """ set the initial value for all the attribute wrapper objects"""
+    def setup_attribute_wrapper(self):
+        """ prepare the caches for attribute wrapper objects"""
 
-        self.value_dict = {i: i.initial_value() for i in self.attr_list()}
+        self._attribute_wrapper_io = {}
 
     def is_attribute_access_allowed(self, req_type):
         """ Returns whether an attribute wrapped by the attribute_wrapper be accessed. """
@@ -153,7 +153,7 @@ class lofar_device(Device, metaclass=DeviceMeta):
         self.set_state(DevState.INIT)
         self.set_status("Device is in the INIT state.")
 
-        self.setup_value_dict()
+        self.setup_attribute_wrapper()
 
         # reload our class & device properties from the Tango database
         self.get_device_properties()
diff --git a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py b/tangostationcontrol/tangostationcontrol/devices/opcua_device.py
index eb2e508c35cf6923fb9d121f729465a4eca621e3..c5fa4d1a791bc2511ae93f9e30091c0c1beba2c8 100644
--- a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/opcua_device.py
@@ -105,10 +105,10 @@ class opcua_device(lofar_device):
         for i in self.attr_list():
             try:
                 if not i.comms_id or i.comms_id == OPCUAConnection:
-                    await i.async_set_comm_client(self.opcua_connection)
+                    await i.async_set_comm_client(self, self.opcua_connection)
             except Exception as e:
                 # use the pass function instead of setting read/write fails
-                i.set_pass_func()
+                i.set_pass_func(self)
                 self.opcua_missing_attributes.append(",".join(self.opcua_connection.get_node_path(i.comms_annotation)))
 
                 logger.warning(f"Error while setting the attribute {i.comms_annotation} read/write function.", exc_info=True)
diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py
index e599563fd4f1ad3120d86da937aac31f27dcc53e..643617cfba8853168cef0ac59911fc744571c2e9 100644
--- a/tangostationcontrol/tangostationcontrol/devices/psoc.py
+++ b/tangostationcontrol/tangostationcontrol/devices/psoc.py
@@ -100,10 +100,10 @@ class PSOC(lofar_device):
         # map an access helper class
         for i in self.attr_list():
             try:
-                i.set_comm_client(self.snmp_manager)
+                i.set_comm_client(self, self.snmp_manager)
             except Exception as e:
                 # use the pass function instead of setting read/write fails
-                i.set_pass_func()
+                i.set_pass_func(self)
                 logger.warning("error while setting the SNMP attribute {} read/write function. {}".format(i, e))
 
         self.snmp_manager.start()
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
index 642b1f7bb6ca9e8c873c430d0c11ec65b683d6b2..c9c3d76338d1b8b7da039a4571355c975dbc6f1e 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
@@ -133,10 +133,10 @@ class Statistics(opcua_device):
         for i in self.attr_list():
             try:
                 if i.comms_id == StatisticsClient:
-                    await i.async_set_comm_client(self.statistics_client)
+                    await i.async_set_comm_client(self, self.statistics_client)
             except Exception as e:
                 # use the pass function instead of setting read/write fails
-                i.set_pass_func()
+                i.set_pass_func(self)
                 logger.warning("error while setting the sst attribute {} read/write function. {}. using pass function instead".format(i, e))
 
         await self.statistics_client.start()
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py
index 3ce57a0ae8660ac040a3cff0f5ef8680c5909ebc..e16ac79e1f991737910d689b6fdc406dce2a0e3d 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/client/test_opcua_client_against_server.py
@@ -88,7 +88,7 @@ class TestClientServer(base.IntegrationAsyncTestCase):
             class attribute(object):
                 dim_x = 1
                 dim_y = 0
-                numpy_type = numpy.double
+                datatype = numpy.double
 
             prot_attr = await test_client.setup_protocol_attribute(["double_R"], attribute())
 
@@ -108,7 +108,7 @@ class TestClientServer(base.IntegrationAsyncTestCase):
             class attribute(object):
                 dim_x = 1
                 dim_y = 0
-                numpy_type = numpy.double
+                datatype = numpy.double
 
             prot_attr = await test_client.setup_protocol_attribute(["double_RW"], attribute())
 
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
index fd45244f870ccf0da97f378891638a17447bb28b..485250131cc8221f4881cec6a158816265ad60a4 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
+++ b/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
@@ -36,7 +36,7 @@ def dev_init(device):
     device.set_state(DevState.INIT)
     device.test_client = test_client(device.Fault)
     for i in device.attr_list():
-        asyncio.run(i.async_set_comm_client(device.test_client))
+        asyncio.run(i.async_set_comm_client(device, device.test_client))
     device.test_client.start()
 
 
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py
index 577bab69e469fb26af2252790b22f7f92d2c0ade..245e03eff42a5592fa3a36673a2d1e01405e680f 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py
+++ b/tangostationcontrol/tangostationcontrol/test/clients/test_client.py
@@ -65,7 +65,7 @@ class test_client(CommClient):
         else:
             dims = (attribute.dim_x,)
 
-        dtype = attribute.numpy_type
+        dtype = attribute.datatype
 
         return dims, dtype
 
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py
index ab20b238297af55adef923c7417273dc07e57dc6..63daef88819ad97ebc7b0cb6ddf2c7bda6c86a75 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py
+++ b/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py
@@ -93,7 +93,7 @@ class TestOPCua(base.AsyncTestCase):
         for i in ATTR_TEST_TYPES:
             class mock_attr:
                 def __init__(self, dtype, x, y):
-                    self.numpy_type = dtype
+                    self.datatype = dtype
                     self.dim_x = x
                     self.dim_y = y