Repository: sogno-platform/dpsim (language: python)

Issue:
Fix attribute relationships for reference voltages
This PR fixes multiple bugs regarding the import of the `mVoltageRef` attribute of the `DP_Ph1_VoltageSource`. For this, the reference relationship with voltageRef attributes in components that include voltage sources is reversed. Additionally, the update function for multiple chained dynamic referencing attributes is fixed to allow proper updating of the data pointers.
lock-free queue for interface to villas etc.
Could be based on this implementation:
https://moodycamel.com/blog/2013/a-fast-lock-free-queue-for-c++.htm

Requirements:
- provide lock-free communication between two threads in dpsim, independent of communication interface implementation
- C++ not C


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml
index 1836ea31fa..369c1bfed1 100644
--- a/.github/workflows/container.yaml
+++ b/.github/workflows/container.yaml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Login to Docker Hub
         uses: docker/login-action@v1 
@@ -30,7 +30,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Login to Docker Hub
         uses: docker/login-action@v1 
@@ -50,7 +50,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Login to Docker Hub
         uses: docker/login-action@v1 
@@ -70,7 +70,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Login to Docker Hub
         uses: docker/login-action@v1 
diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml
index e470d99685..66245b83ad 100644
--- a/.github/workflows/generate.yaml
+++ b/.github/workflows/generate.yaml
@@ -11,7 +11,7 @@ jobs:
     container: sogno/dpsim:dev
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Create Build Environment
       run: mkdir build
diff --git a/.github/workflows/run_and_profile_example.yaml b/.github/workflows/run_and_profile_example.yaml
index 2837a5b3ca..9694649110 100644
--- a/.github/workflows/run_and_profile_example.yaml
+++ b/.github/workflows/run_and_profile_example.yaml
@@ -20,7 +20,7 @@ jobs:
     container: sogno/dpsim:dev-centos
     steps:
     - name: Restore Cache
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/build
         key: build-cache-centos-profiling-${{ github.sha }}
@@ -40,7 +40,7 @@ jobs:
       run: gprof ${{ inputs.path }} | gprof2dot -s | dot -Tpng -o profiling.png || true
 
     - name: Archive profiler output
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: profiler-output-${{ inputs.name }}
         path: |
diff --git a/.github/workflows/run_villas_example.yaml b/.github/workflows/run_villas_example.yaml
index 4a9fd25115..dc2eba3bb5 100644
--- a/.github/workflows/run_villas_example.yaml
+++ b/.github/workflows/run_villas_example.yaml
@@ -23,7 +23,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Restore build archive
       uses: actions/download-artifact@v3
diff --git a/.github/workflows/sonar_cloud.yaml b/.github/workflows/sonar_cloud.yaml
index 6877f5188d..cf9d5a4b3d 100644
--- a/.github/workflows/sonar_cloud.yaml
+++ b/.github/workflows/sonar_cloud.yaml
@@ -13,7 +13,7 @@ jobs:
       SONAR_SERVER_URL: "https://sonarcloud.io"
       BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
         with:
           fetch-depth: 0  # Shallow clones should be disabled for a better relevancy of analysis
       - name: Set up JDK 11
@@ -36,7 +36,7 @@ jobs:
           unzip -o $HOME/.sonar/build-wrapper-linux-x86.zip -d $HOME/.sonar/
           echo "$HOME/.sonar/build-wrapper-linux-x86" >> $GITHUB_PATH
       - name: Setup sonar cache
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ${{ github.workspace }}/sonar-cache
           key: sonar-cache-${{ github.ref }}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6239f51401..ae32a5c8b8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -126,6 +126,7 @@ find_package(Graphviz)
 find_package(VILLASnode)
 find_package(MAGMA)
 
+include(cmake/GetReaderWriterQueue.cmake)
 
 find_package (Python COMPONENTS Interpreter Development)
 if (WITH_PYBIND_MODULE)
diff --git a/cmake/GetReaderWriterQueue.cmake b/cmake/GetReaderWriterQueue.cmake
new file mode 100644
index 0000000000..2d4b1ae063
--- /dev/null
+++ b/cmake/GetReaderWriterQueue.cmake
@@ -0,0 +1,18 @@
+include(FetchContent)
+
+FetchContent_Declare(
+  readerwriterqueue
+  GIT_REPOSITORY    https://github.com/cameron314/readerwriterqueue
+  GIT_TAG           v1.0.6
+)
+
+# TODO: Is this really still necessary? Or can we just use cmake 3.14?
+if(${CMAKE_VERSION} VERSION_LESS "3.14.0")
+  FetchContent_GetProperties(readerwriterqueue)
+  if(NOT readerwriterqueue_POPULATED)
+    FetchContent_Populate(readerwriterqueue)
+    add_subdirectory(${readerwriterqueue_SOURCE_DIR} ${readerwriterqueue_BINARY_DIR})
+  endif()
+else()
+  FetchContent_MakeAvailable(readerwriterqueue)
+endif()
\ No newline at end of file
diff --git a/dpsim-models/include/dpsim-models/Attribute.h b/dpsim-models/include/dpsim-models/Attribute.h
index e09cce24d8..d2eba045db 100644
--- a/dpsim-models/include/dpsim-models/Attribute.h
+++ b/dpsim-models/include/dpsim-models/Attribute.h
@@ -103,6 +103,10 @@ namespace CPS {
 				return mPtr;
 			}
 
+			bool isNull() const {
+				return mPtr == nullptr;
+			}
+
 			/*
 			These (implicit) comparison operators are disabled to avoid accidentally comparing pointers instead of attribute values.
 			When a pointer comparison is necessary, this can be done via the `getPtr` method or by using the `AttributeCmp` and `AttributeEq` structs. 
@@ -141,7 +145,7 @@ namespace CPS {
 		}
 	};
 
-		/**
+	/**
 	 * Struct providing an (explicit) equals function for Attribute Pointers. Can be used in STL containers.
 	 * */
 	template<class T>
@@ -174,7 +178,24 @@ namespace CPS {
 
 		virtual ~AttributeBase() = default;
 		
-		
+		/**
+		 * @brief Copy the attribute value of `copyFrom` onto this attribute
+		 * @return true if the copy operation was successful, false otherwise
+		 */
+		virtual bool copyValue(AttributeBase::Ptr copyFrom) = 0;
+
+		/**
+		 * @brief Get the type of this attribute
+		 * @return std::type_info 
+		 */
+		virtual const std::type_info& getType() = 0;
+
+		/**
+		 * @brief Generates a new attribute of the same type and copies the current value in the heap. Does not copy any dependency relations!
+		 * @return Pointer to the copied attribute
+		 */
+		virtual AttributeBase::Ptr cloneValueOntoNewAttribute() = 0;
+
 		/**
 		 * Append all dependencies of this attribute to the given set.
 		 * For static attributes, this will only append `this`, for dynamic attributes, it will recursively collect and append
@@ -256,8 +277,8 @@ namespace CPS {
 		std::shared_ptr<T> mData;
 
 	public:
-		typedef T Type;
-		typedef AttributePointer<Attribute<T>> Ptr;
+		using Type = T;
+		using Ptr = AttributePointer<Attribute<T>>;
 
 		Attribute(T initialValue = T()) :
 			AttributeBase(), mData(std::make_shared<T>()) {
@@ -309,7 +330,7 @@ namespace CPS {
 		virtual std::shared_ptr<T> asRawPointer() = 0;
 
 		/// Fallback method for all attribute types not covered by the specifications in Attribute.cpp
-		virtual String toString() override {
+		String toString() override {
 			std::stringstream ss;
 			ss << this->get();
 			return ss.str();
@@ -335,6 +356,35 @@ namespace CPS {
 			return this->get();
 		}
 
+		/**
+		 * @brief Copy the attribute value of `copyFrom` onto this attribute
+		 * @return true if the copy operation was successful, false otherwise
+		 */
+		bool copyValue(AttributeBase::Ptr copyFrom) override {
+			Attribute<T>::Ptr copyFromTyped = std::dynamic_pointer_cast<Attribute<T>>(copyFrom.getPtr());
+			if (copyFromTyped.getPtr() == nullptr) {
+				return false;
+			}
+			this->set(**copyFromTyped);
+			return true;
+		}
+
+		/**
+		 * @brief Get the type of this attribute
+		 * @return std::type_info 
+		 */
+		const std::type_info& getType() override {
+			return typeid(T);
+		}
+
+		/**
+		 * @brief Generates a new attribute of the same type and copies the current value in the heap. Does not copy any dependency relations!
+		 * @return Pointer to the copied attribute
+		 */
+		AttributeBase::Ptr cloneValueOntoNewAttribute() override {
+			return AttributePointer<AttributeBase>(AttributeStatic<T>::make(this->get()));
+		};
+
 		/**
 		 * General method for deriving a new attribute from this attribute. Custom getter and setter functions have to be provided. The newly created
 		 * attribute will only depend on this attribute in a 1:1 relationship.
diff --git a/dpsim-villas/examples/cxx/CMakeLists.txt b/dpsim-villas/examples/cxx/CMakeLists.txt
index 5c626bd76c..00b7d280c2 100644
--- a/dpsim-villas/examples/cxx/CMakeLists.txt
+++ b/dpsim-villas/examples/cxx/CMakeLists.txt
@@ -16,21 +16,21 @@ set(SHMEM_SOURCES
 	FileExample.cpp
 	MqttExample.cpp
 	SharedMemExample.cpp
-	ShmemExample.cpp
-	ShmemDistributedReference.cpp
-	ShmemDistributedDirect.cpp
-	ShmemDistributedVillas.cpp
-	ShmemControllableSource.cpp
-	ShmemControllableFiltSource.cpp
-	Shmem_CIGRE_MV_PowerFlowTest.cpp
-	Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp
+	#ShmemExample.cpp
+	#ShmemDistributedReference.cpp
+	#ShmemDistributedDirect.cpp
+	#ShmemDistributedVillas.cpp
+	#ShmemControllableSource.cpp
+	#ShmemControllableFiltSource.cpp
+	#Shmem_CIGRE_MV_PowerFlowTest.cpp
+	#Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp
 )
 
 if(WITH_CIM)
 	set(CIM_SHMEM_SOURCES
-		Shmem_WSCC-9bus.cpp
-		Shmem_WSCC-9bus_Ctrl.cpp
-		Shmem_WSCC-9bus_CtrlDist.cpp
+		#Shmem_WSCC-9bus.cpp
+		#Shmem_WSCC-9bus_Ctrl.cpp
+		#Shmem_WSCC-9bus_CtrlDist.cpp
 	)
 endif()
 
@@ -44,7 +44,7 @@ foreach(SOURCE ${SHMEM_SOURCES} ${CIM_SHMEM_SOURCES})
 	target_compile_options(${TARGET} PUBLIC ${DPSIM_CXX_FLAGS})
 endforeach()
 
-add_executable(ShmemRealtimeExample ShmemExample.cpp)
-target_link_libraries(ShmemRealtimeExample ${LIBRARIES})
-target_include_directories(ShmemRealtimeExample PRIVATE ${INCLUDE_DIRS})
-target_compile_definitions(ShmemRealtimeExample PUBLIC REALTIME)
+#add_executable(ShmemRealtimeExample ShmemExample.cpp)
+#target_link_libraries(ShmemRealtimeExample ${LIBRARIES})
+#target_include_directories(ShmemRealtimeExample PRIVATE ${INCLUDE_DIRS})
+#target_compile_definitions(ShmemRealtimeExample PUBLIC REALTIME)
diff --git a/dpsim-villas/examples/cxx/FileExample.cpp b/dpsim-villas/examples/cxx/FileExample.cpp
index c48daf78f7..51376ec5d8 100644
--- a/dpsim-villas/examples/cxx/FileExample.cpp
+++ b/dpsim-villas/examples/cxx/FileExample.cpp
@@ -59,12 +59,12 @@ int main(int argc, char* argv[]) {
         }
     })STRING";
 
-    InterfaceVillas intf("dpsim-file", fileConfig);
+    auto intf = std::make_shared<InterfaceVillas>(fileConfig);
 
 	// Interface
 	//evs->setAttributeRef("V_ref", intf.importComplex(0));
-	intf.exportComplex(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
-	sim.addInterface(&intf);
+	intf->exportAttribute(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0, true);
+	sim.addInterface(intf);
 
 	// Logger
 	auto logger = DataLogger::make(simName);
diff --git a/dpsim-villas/examples/cxx/MqttExample.cpp b/dpsim-villas/examples/cxx/MqttExample.cpp
index d2bd17e185..1d259d483f 100644
--- a/dpsim-villas/examples/cxx/MqttExample.cpp
+++ b/dpsim-villas/examples/cxx/MqttExample.cpp
@@ -14,7 +14,7 @@ int main(int argc, char* argv[]) {
 	// Voltage is read from VILLASnode and current through everything is written back.
 	String simName = "Mqtt_example";
 	CPS::Logger::setLogDir("logs/"+simName);
-	Real timeStep = 0.1;
+	Real timeStep = 0.01;
 
 	// Nodes
 	auto n1 = SimNode::make("n1");
@@ -48,7 +48,7 @@ int main(int argc, char* argv[]) {
 	RealTimeSimulation sim(simName);
 	sim.setSystem(sys);
 	sim.setTimeStep(timeStep);
-	sim.setFinalTime(2.0);
+	sim.setFinalTime(10.0);
 	
     std::string mqttConfig = R"STRING({
         "type": "mqtt",
@@ -62,13 +62,13 @@ int main(int argc, char* argv[]) {
         }
     })STRING";
 
-    InterfaceVillas intf("dpsim-mqtt", mqttConfig);
+    auto intf = std::make_shared<InterfaceVillas>(mqttConfig);
 
 	// Interface
-	evs->mVoltageRef->setReference(intf.importComplex(0));
-	intf.exportComplex(evs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0, "v_src");
-	intf.exportComplex(rL->mIntfVoltage->deriveCoeff<Complex>(0, 0), 1, "v_load");
-	sim.addInterface(&intf, true);
+	sim.addInterface(intf);
+	intf->importAttribute(evs->mVoltageRef, 0, true, true);
+	intf->exportAttribute(evs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0, true, "v_src");
+	intf->exportAttribute(ll->mIntfCurrent->deriveCoeff<Complex>(0, 0), 1, true, "v_load");
 
 	// Logger
 	auto logger = DataLogger::make(simName);
@@ -77,6 +77,7 @@ int main(int argc, char* argv[]) {
 	logger->logAttribute("v3", n3->mVoltage);
 	logger->logAttribute("v4", n4->mVoltage);
 	logger->logAttribute("v_src", evs->mVoltageRef);
+	logger->logAttribute("i_r", rl->mIntfCurrent, 1, 1);
 	logger->logAttribute("i_evs", evs->mIntfCurrent, 1, 1);
 	logger->logAttribute("v_evs", evs->mIntfVoltage, 1, 1);
 	sim.addLogger(logger);
diff --git a/dpsim-villas/examples/cxx/SharedMemExample.cpp b/dpsim-villas/examples/cxx/SharedMemExample.cpp
index 806628276c..c7330b4839 100644
--- a/dpsim-villas/examples/cxx/SharedMemExample.cpp
+++ b/dpsim-villas/examples/cxx/SharedMemExample.cpp
@@ -62,13 +62,13 @@ int main(int argc, char* argv[]) {
 		"queuelen": 1024
     })STRING";
 
-    InterfaceVillas intf("dpsim-shmem", shmemConfig);
+    auto intf = std::make_shared<InterfaceVillas>(shmemConfig);
 
 	// Interface
 	//evs->setAttributeRef("V_ref", intf.importComplex(0));
-	intf.exportComplex(evs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0, "v_src");
-	intf.exportComplex(rL->mIntfVoltage->deriveCoeff<Complex>(0, 0), 1, "v_load");
-	sim.addInterface(&intf, true);
+	intf->exportAttribute(evs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0, true, "v_src");
+	intf->exportAttribute(rL->mIntfVoltage->deriveCoeff<Complex>(0, 0), 1, true, "v_load");
+	sim.addInterface(intf);
 
 	// Logger
 	auto logger = DataLogger::make(simName);
diff --git a/dpsim-villas/examples/cxx/ShmemControllableFiltSource.cpp b/dpsim-villas/examples/cxx/ShmemControllableFiltSource.cpp
index e648af60d1..16041dd720 100644
--- a/dpsim-villas/examples/cxx/ShmemControllableFiltSource.cpp
+++ b/dpsim-villas/examples/cxx/ShmemControllableFiltSource.cpp
@@ -56,7 +56,7 @@ int main(int argc, char *argv[]) {
 	sim.setDomain(Domain::DP);
 	sim.setSolverType(Solver::Type::MNA);
 
-	sim.addInterface(&intf);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 	sim.run();
 
 	return 0;
diff --git a/dpsim-villas/examples/cxx/ShmemControllableSource.cpp b/dpsim-villas/examples/cxx/ShmemControllableSource.cpp
index c2f0295353..71acc20d6c 100644
--- a/dpsim-villas/examples/cxx/ShmemControllableSource.cpp
+++ b/dpsim-villas/examples/cxx/ShmemControllableSource.cpp
@@ -26,8 +26,8 @@ int main(int argc, char *argv[]) {
 	ecs->connect({ SimNode::GND, n1 });
 	r1->connect({ SimNode::GND, n1 });
 
-	ecs->mCurrentRef->setReference(intf.importComplex(0));
-	intf.exportComplex(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
+	intf.importAttribute(ecs->mCurrentRef, 0);
+	intf.exportAttribute(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
 
 	auto sys = SystemTopology(50, SystemNodeList{n1}, SystemComponentList{ecs, r1});
 	
@@ -38,7 +38,7 @@ int main(int argc, char *argv[]) {
 	sim.setDomain(Domain::DP);
 	sim.setSolverType(Solver::Type::MNA);
 
-	sim.addInterface(&intf);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 	sim.run();
 
 	return 0;
diff --git a/dpsim-villas/examples/cxx/ShmemDistributedDirect.cpp b/dpsim-villas/examples/cxx/ShmemDistributedDirect.cpp
index d1ca6f23bd..1bbf58441d 100644
--- a/dpsim-villas/examples/cxx/ShmemDistributedDirect.cpp
+++ b/dpsim-villas/examples/cxx/ShmemDistributedDirect.cpp
@@ -79,10 +79,10 @@ int main(int argc, char *argv[]) {
 
 		// Map attributes to interface entries
 		InterfaceShmem intf(in, out);
-		evs->mVoltageRef->setReference(intf.importComplex(0));
+		intf.importAttribute(evs->mVoltageRef, 0);
 		auto evsAttrMinus = evs->mIntfCurrent->deriveCoeff<Complex>(0,0);
-		intf.exportComplex(evsAttrMinus, 0);
-		sim.addInterface(&intf);
+		intf.exportAttribute(evsAttrMinus, 0);
+		sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 		MatrixComp initialEvsCurrent = MatrixComp::Zero(1,1);
 		initialEvsCurrent(0,0) = Complex(5,0);
@@ -126,10 +126,10 @@ int main(int argc, char *argv[]) {
 
 		// Map attributes to interface entries
 		InterfaceShmem intf(in, out);
-		ecs->mCurrentRef->setReference(intf.importComplex(0));
+		intf.importAttribute(ecs->mCurrentRef, 0);
 		//intf.exportComplex(ecs->mIntfVoltage->coeff(0, 0), 0);
-		intf.exportComplex(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0)->deriveScaled(Complex(-1.,0)), 0);
-		sim.addInterface(&intf);
+		intf.exportAttribute(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0)->deriveScaled(Complex(-1.,0)), 0);
+		sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 		sim.run();
 	}
diff --git a/dpsim-villas/examples/cxx/ShmemDistributedVillas.cpp b/dpsim-villas/examples/cxx/ShmemDistributedVillas.cpp
index 95a4b31af3..aae2572850 100644
--- a/dpsim-villas/examples/cxx/ShmemDistributedVillas.cpp
+++ b/dpsim-villas/examples/cxx/ShmemDistributedVillas.cpp
@@ -56,8 +56,8 @@ int main(int argc, char *argv[]) {
 		comps = SystemComponentList{evs, vs, l1, r1};
 		nodes = SystemNodeList{SimNode::GND, n1, n2, n3};
 
-		evs->mVoltageRef->setReference(intf.importComplex(0));
-		intf.exportComplex(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
+		intf.importAttribute(evs->mVoltageRef, 0);
+		intf.exportAttribute(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
 
 	}
 	else if (String(argv[1]) == "1") {
@@ -86,8 +86,8 @@ int main(int argc, char *argv[]) {
 		comps = SystemComponentList{ecs, sw, r2A, r2B};
 		nodes = SystemNodeList{SimNode::GND, n4, n5};
 
-		ecs->mCurrentRef->setReference(intf.importComplex(0));
-		intf.exportComplex(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
+		intf.importAttribute(ecs->mCurrentRef, 0);
+		intf.exportAttribute(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
 	}
 	else {
 		std::cerr << "invalid test number" << std::endl;
@@ -103,7 +103,7 @@ int main(int argc, char *argv[]) {
 	sim.setSystem(sys);
 	sim.setTimeStep(timeStep);
 	sim.setFinalTime(20);
-	sim.addInterface(&intf);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 	if (String(argv[1]) == "1") {
 		auto evt = SwitchEvent::make(10, sys.component<CPS::Base::Ph1::Switch>("sw"), true);
diff --git a/dpsim-villas/examples/cxx/ShmemExample.cpp b/dpsim-villas/examples/cxx/ShmemExample.cpp
index 3ce34fc174..c26affb4c3 100644
--- a/dpsim-villas/examples/cxx/ShmemExample.cpp
+++ b/dpsim-villas/examples/cxx/ShmemExample.cpp
@@ -60,9 +60,9 @@ int main(int argc, char* argv[]) {
 #endif
 
 	// Interface
-	evs->mVoltageRef->setReference(intf.importComplex(0));
-	intf.exportComplex(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
-	sim.addInterface(&intf);
+	intf.importAttribute(evs->mVoltageRef, 0);
+	intf.exportAttribute(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 	// Logger
 	auto logger = DataLogger::make(simName);
diff --git a/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest.cpp b/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest.cpp
index 47c7a37f1a..bfab04d775 100644
--- a/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest.cpp
+++ b/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest.cpp
@@ -57,7 +57,7 @@ int main(int argc, char** argv) {
 		intf.exportReal(v->derivePhase(), (i*2)+1); o++;
 	}
 
-	sim.addInterface(&intf, false);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 	sim.run(std::chrono::seconds(5));
 
diff --git a/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp b/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp
index 44cb120dc7..accfb9fbb8 100644
--- a/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp
+++ b/dpsim-villas/examples/cxx/Shmem_CIGRE_MV_PowerFlowTest_LoadProfiles.cpp
@@ -103,7 +103,7 @@ int main(int argc, char** argv){
 	}
     villas_conf.close();
 
-	sim.addInterface(&intf, false);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 
 	sim.run(std::chrono::seconds(5));
 
diff --git a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus.cpp b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus.cpp
index 1996c60196..735d643423 100644
--- a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus.cpp
+++ b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus.cpp
@@ -45,7 +45,7 @@ int main(int argc, char *argv[]) {
 		o += 2;
 	}
 
-	sim.addInterface(&intf);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 	sim.run();
 
 	return 0;
diff --git a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_Ctrl.cpp b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_Ctrl.cpp
index b97ecd6348..c4d2f349ab 100644
--- a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_Ctrl.cpp
+++ b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_Ctrl.cpp
@@ -104,7 +104,7 @@ int main(int argc, char *argv[]) {
 	intf.exportReal(load->attribute<Real>("P"), o++);
 	intf.exportReal(load_profile->attribute<Real>("P"), o++);
 
-	sim.addInterface(&intf, false);
+	sim.addInterface(std::shared_ptr<Interface>(&intf));
 	sim.addLogger(logger);
 	sim.run(args.startTime);
 
diff --git a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_CtrlDist.cpp b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_CtrlDist.cpp
index 80a2464c12..3bd62d1d9c 100644
--- a/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_CtrlDist.cpp
+++ b/dpsim-villas/examples/cxx/Shmem_WSCC-9bus_CtrlDist.cpp
@@ -54,8 +54,8 @@ int main(int argc, char *argv[]) {
 
 		InterfaceShmem intf1("/dpsim01", "/dpsim10", nullptr, false);
 		InterfaceShmem intf2("/dpsim1-villas", "/villas-dpsim1", nullptr, false);
-		sim.addInterface(&intf1);
-		sim.addInterface(&intf2, false);
+		sim.addInterface(std::shared_ptr<Interface>(&intf1));
+		sim.addInterface(std::shared_ptr<Interface>(&intf2));
 
 		// Controllers and filter
 		std::vector<Real> coefficients_profile = std::vector<Real>(2000, 1./2000);
@@ -65,8 +65,8 @@ int main(int argc, char *argv[]) {
 		sys.mComponents.push_back(filtP_profile);
 
 		// Register interface current source and voltage drop
-		ecs->mCurrentRef->setReference(intf1.importComplex(0));
-		intf1.exportComplex(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
+		intf1.importAttribute(ecs->mCurrentRef, 0);
+		intf1.exportAttribute(ecs->mIntfVoltage->deriveCoeff<Complex>(0, 0), 0);
 
 		// TODO: gain by 20e8
 		filtP_profile->setInput(intf2.importReal(0));
@@ -118,16 +118,16 @@ int main(int argc, char *argv[]) {
 		sim.setFinalTime(args.duration);
 
 		InterfaceShmem intf1("/dpsim10", "/dpsim01", nullptr, false);
-		sim.addInterface(&intf1);
+		sim.addInterface(std::shared_ptr<Interface>(&intf1));
 
 		InterfaceShmem intf2("/dpsim2-villas", "/villas-dpsim2", nullptr, false);
-		sim.addInterface(&intf2, false);
+		sim.addInterface(std::shared_ptr<Interface>(&intf2));
 
 		// Register voltage source reference and current flowing through source
 		// multiply with -1 to consider passive sign convention
-		evs->mVoltageRef->setReference(intf1.importComplex(0));
+		intf1.importAttribute(evs->mVoltageRef, 0);
 		// TODO: invalid sign
-		intf1.exportComplex(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
+		intf1.exportAttribute(evs->mIntfCurrent->deriveCoeff<Complex>(0, 0), 0);
 
 		// Register controllable load
 		filtP->setInput(intf2.importReal(0));
diff --git a/dpsim-villas/include/dpsim-villas/InterfaceSampleBased.h b/dpsim-villas/include/dpsim-villas/InterfaceSampleBased.h
deleted file mode 100644
index 5a73ba78a0..0000000000
--- a/dpsim-villas/include/dpsim-villas/InterfaceSampleBased.h
+++ /dev/null
@@ -1,127 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#pragma once
-
-#include <vector>
-#include <map>
-
-#include <dpsim-models/PtrFactory.h>
-#include <dpsim/Interface.h>
-
-#include <villas/sample.hpp>
-#include <villas/node.hpp>
-#include <villas/exceptions.hpp>
-#include <villas/signal.hpp>
-#include <villas/signal_list.hpp>
-
-using namespace villas;
-
-namespace DPsim {
-	class InterfaceSampleBased :
-		public Interface,
-		public SharedFactory<InterfaceSampleBased> {
-
-	public:
-		typedef std::shared_ptr<InterfaceSampleBased> Ptr;
-		typedef struct node::Sample Sample;
-
-	protected:
-		// Using std::function / lambda makes the other template code nicer, but from
-		// the outside, only the attribute-based functions should be used to
-		// guarantee proper scheduling
-
-		void addImport(std::function<void(Sample*)> l) { mImports.push_back(l); }
-		void addExport(std::function<void(Sample*)> l) { mExports.push_back(l); }
-
-		std::vector<std::function<void(Sample*)>> mExports, mImports;
-		CPS::AttributeBase::List mExportAttrs, mImportAttrs;
-
-		Sample *mLastSample;
-		bool mOpened;
-		String mRName, mWName;
-		int mSequence;
-
-		CPS::Logger::Log mLog;
-
-		/// Is this InterfaceVillas used for synchronization?
-		bool mSync;
-		/// Downsampling
-		UInt mDownsampling;
-		std::map<int, node::Signal::Ptr> mExportSignals;
-		std::map<int, node::Signal::Ptr> mImportSignals;
-
-	public:
-
-		class PreStep : public CPS::Task {
-		public:
-			PreStep(InterfaceSampleBased& intf) :
-				Task(intf.mRName + ".Read"), mIntf(intf) {
-				for (auto attr : intf.mImportAttrs) {
-					mModifiedAttributes.push_back(attr);
-				}
-				mModifiedAttributes.push_back(Scheduler::external);
-			}
-
-			void execute(Real time, Int timeStepCount);
-
-		private:
-			InterfaceSampleBased& mIntf;
-		};
-
-		class PostStep : public CPS::Task {
-		public:
-			PostStep(InterfaceSampleBased& intf) :
-				Task(intf.mWName+ ".Write"), mIntf(intf) {
-				for (auto attr : intf.mExportAttrs) {
-					mAttributeDependencies.push_back(attr);
-				}
-				mModifiedAttributes.push_back(Scheduler::external);
-			}
-
-			void execute(Real time, Int timeStepCount);
-
-		private:
-			InterfaceSampleBased& mIntf;
-		};
-
-		InterfaceSampleBased(const String &wn, const String &rn, Bool sync = true, UInt downsampling = 1) :
-            mOpened(false),
-            mRName(rn),
-            mWName(wn),
-	        mSync(sync),
-	        mDownsampling(downsampling) {}
-			
-		virtual ~InterfaceSampleBased() {
-			if (mOpened)
-				close();
-		}
-
-		virtual void open(CPS::Logger::Log log) = 0;
-		virtual void close();
-
-		CPS::Attribute<Int>::Ptr importInt(UInt idx);
-		CPS::Attribute<Real>::Ptr importReal(UInt idx);
-		CPS::Attribute<Bool>::Ptr importBool(UInt idx);
-		CPS::Attribute<Complex>::Ptr importComplex(UInt idx);
-		CPS::Attribute<Complex>::Ptr importComplexMagPhase(UInt idx);
-
-		void exportInt(CPS::Attribute<Int>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="");
-		void exportReal(CPS::Attribute<Real>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="");
-		void exportBool(CPS::Attribute<Bool>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="");
-		void exportComplex(CPS::Attribute<Complex>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="");
-
-		/** Read data for a timestep from the InterfaceVillas and passes the values
-		 * to all registered current / voltage sources.
-		 */
-		virtual void readValues(bool blocking = true) = 0;
-
-		/** Write all exported values to the InterfaceVillas. Called after every timestep.
-		 * @param model Reference to the system model which should be used to
-		 * calculate needed voltages.
-		 */
-		virtual void writeValues() = 0;
-
-		CPS::Task::List getTasks();
-	};
-}
-
diff --git a/dpsim-villas/include/dpsim-villas/InterfaceShmem.h b/dpsim-villas/include/dpsim-villas/InterfaceShmem.h
deleted file mode 100644
index ce2924667c..0000000000
--- a/dpsim-villas/include/dpsim-villas/InterfaceShmem.h
+++ /dev/null
@@ -1,52 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#pragma once
-
-#include <dpsim-villas/InterfaceSampleBased.h>
-
-#include <villas/shmem.hpp>
-
-using namespace villas;
-
-namespace DPsim {
-	/// Shmem interface used in combination with VILLAS
-	class InterfaceShmem :
-		public InterfaceSampleBased,
-		public SharedFactory<InterfaceShmem> {
-
-	public:
-		typedef std::shared_ptr<InterfaceShmem> Ptr;
-		typedef struct node::ShmemConfig Config;
-		typedef struct node::ShmemInterface ShmemInt;
-
-	protected:
-		ShmemInt mShmem;
-		Config mConf;
-
-	public:
-		/** Create a InterfaceShmem with a specific configuration for the output queue.
-		 *
-		 * @param wname The name of the POSIX shmem object where samples will be written to.
-		 * @param rname The name of the POSIX shmem object where samples will be read from.
-		 * @param conf The configuration object for the output queue (see VILLASnode's documentation), or nullptr for sensible defaults.
-		 */
-		InterfaceShmem(const String &wn, const String &rn, Config *conf = nullptr, Bool sync = true, UInt downsampling = 1) :
-			InterfaceSampleBased(wn, rn, sync, downsampling)
-		{
-			if (conf != nullptr) {
-				mConf = *conf;
-			} else {
-				mConf.queuelen = 512;
-				mConf.samplelen = 64;
-				mConf.polling = 0;
-			}
-		}
-
-		void open(CPS::Logger::Log log);
-		void close();
-
-		void readValues(bool blocking = true);
-		void writeValues();
-	};
-}
-
diff --git a/dpsim-villas/include/dpsim-villas/InterfaceVillas.h b/dpsim-villas/include/dpsim-villas/InterfaceVillas.h
index ed305f3c01..98b31dab74 100644
--- a/dpsim-villas/include/dpsim-villas/InterfaceVillas.h
+++ b/dpsim-villas/include/dpsim-villas/InterfaceVillas.h
@@ -2,55 +2,50 @@
 
 #pragma once
 
-#include <dpsim-villas/InterfaceSampleBased.h>
+#include <dpsim-models/PtrFactory.h>
+#include <dpsim/Interface.h>
 
 #include <villas/node.hpp>
 #include <villas/exceptions.hpp>
 #include <villas/memory.hpp>
 #include <villas/kernel/rt.hpp>
 #include <villas/pool.hpp>
+#include <villas/sample.hpp>
+#include <villas/signal.hpp>
+#include <villas/signal_list.hpp>
 
 using namespace villas;
 
 namespace DPsim {
+	/// Interface type that can be used to import and export simulation attributes over any node type supported by VILLASnode
 	class InterfaceVillas :
-		public InterfaceSampleBased,
+		public Interface,
 		public SharedFactory<InterfaceVillas> {
 
 	public:
-		typedef std::shared_ptr<InterfaceVillas> Ptr;
-		static UInt villasPriority;
-		static UInt villasAffinity;
-		static UInt villasHugePages;
+		/// @brief create a new InterfaceVillas instance
+		/// @param nodeConfig VILLASnode node configuration in JSON format
+		/// @param queueLength queue lenght configured for the node
+		/// @param sampleLength sample length configured for the node
+		/// @param name Name of this interface. Currently only used for naming the simulation tasks
+		/// @param downsampling Only import and export attributes on every nth timestep
+		InterfaceVillas(const String &nodeConfig, UInt queueLength = 512, UInt sampleLength = 64, const String& name = "", UInt downsampling = 1);
+
+		/// @brief configure an attribute import
+		/// @param attr the attribute that should be updated with the imported values
+		/// @param idx The id given to the attribute within VILLASnode samples
+		/// @param blockOnRead Whether the simulation should block on every import until the attribute has been updated
+		/// @param syncOnSimulationStart Whether the simulation should block before the first timestep until this attribute has been updated
+        void importAttribute(CPS::AttributeBase::Ptr attr, UInt idx, Bool blockOnRead = false, Bool syncOnSimulationStart = true);
+		
+		/// @brief configure an attribute export
+		/// @param attr the attribute which's value should be exported
+		/// @param idx The id given to the attribute within VILLASnode samples
+		/// @param waitForOnWrite Whether a sample that is sent from this interface is required to contain an updated value of this attribute
+		/// @param name Name given to the attribute within VILLASnode samples
+		/// @param unit Unit given to the attribute within VILLASnode samples
+		void exportAttribute(CPS::AttributeBase::Ptr attr, UInt idx, Bool waitForOnWrite, const String& name = "", const String& unit = "");
 
-	protected:
-		//Villas node to send / receive data to / from
-		String mNodeConfig;
-		node::Node* mNode;
-
-		int mQueueLength;
-		int mSampleLength;
-		node::Pool mSamplePool;
-
-		static Bool villasInitialized;
-
-	public:
-		/** Create a InterfaceVillas with a specific configuration for the VillasNode
-		 *
-		 * @param name The name of the newly created VillasNode
-		 */
-		InterfaceVillas(const String &name, const String &nodeConfig, UInt queueLenght = 512, UInt sampleLenght = 64, UInt downsampling = 1);
-
-		void open(CPS::Logger::Log log);
-		void close();
-
-		void readValues(bool blocking = true);
-		void writeValues();
-		void initVillas();
-
-	private:
-		void prepareNode();
-		void setupNodeSignals();
 	};
 }
 
diff --git a/dpsim-villas/include/dpsim-villas/InterfaceWorkerVillas.h b/dpsim-villas/include/dpsim-villas/InterfaceWorkerVillas.h
new file mode 100644
index 0000000000..8354d5ed66
--- /dev/null
+++ b/dpsim-villas/include/dpsim-villas/InterfaceWorkerVillas.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <dpsim-models/PtrFactory.h>
+#include <dpsim/InterfaceWorker.h>
+
+#include <villas/node.hpp>
+#include <villas/exceptions.hpp>
+#include <villas/memory.hpp>
+#include <villas/kernel/rt.hpp>
+#include <villas/pool.hpp>
+#include <villas/sample.hpp>
+#include <villas/signal.hpp>
+#include <villas/signal_list.hpp>
+
+using namespace villas;
+
+namespace DPsim {
+	class InterfaceWorkerVillas :
+		public InterfaceWorker,
+		public SharedFactory<InterfaceWorkerVillas> {
+
+	public:
+		using Ptr = std::shared_ptr<InterfaceWorkerVillas>;
+		using Sample = struct node::Sample;
+
+		static UInt villasPriority;
+		static UInt villasAffinity;
+		static UInt villasHugePages;
+
+	private:
+		static Bool villasInitialized;
+
+		std::vector<std::tuple<std::function<CPS::AttributeBase::Ptr(Sample*)>, UInt>> mImports;
+		std::vector<std::tuple<std::function<void(CPS::AttributeBase::Ptr, Sample*)>, UInt, Bool>> mExports;
+		
+		//Villas node to send / receive data to / from
+		String mNodeConfig;
+		node::Node* mNode;
+
+		int mQueueLength;
+		int mSampleLength;
+		node::Pool mSamplePool;
+
+		Sample *mLastSample;
+		int mSequence;
+
+		std::map<int, node::Signal::Ptr> mExportSignals;
+		std::map<int, node::Signal::Ptr> mImportSignals;
+
+	public:
+
+		InterfaceWorkerVillas(const String &nodeConfig, UInt queueLenght = 512, UInt sampleLenght = 64);
+
+		void open() override;
+		void close() override;
+	
+		void readValuesFromEnv(std::vector<Interface::AttributePacket>& updatedAttrs) override;
+		void writeValuesToEnv(std::vector<Interface::AttributePacket>& updatedAttrs) override;
+
+        virtual void configureImport(UInt attributeId, const std::type_info& type, UInt idx);
+        virtual void configureExport(UInt attributeId, const std::type_info& type, UInt idx, Bool waitForOnWrite, const String& name = "", const String& unit = "");
+	
+	private:
+		void prepareNode();
+		void setupNodeSignals();
+		void initVillas() const;
+	};
+}
+
diff --git a/dpsim-villas/src/CMakeLists.txt b/dpsim-villas/src/CMakeLists.txt
index 347de86376..a7318532b1 100644
--- a/dpsim-villas/src/CMakeLists.txt
+++ b/dpsim-villas/src/CMakeLists.txt
@@ -1,7 +1,6 @@
 set(DPSIM_VILLAS_SOURCES
-    InterfaceSampleBased.cpp
-    InterfaceShmem.cpp
     InterfaceVillas.cpp
+    InterfaceWorkerVillas.cpp
 )
 
 add_library(dpsim-villas ${DPSIM_VILLAS_SOURCES})
diff --git a/dpsim-villas/src/InterfaceSampleBased.cpp b/dpsim-villas/src/InterfaceSampleBased.cpp
deleted file mode 100644
index f0231d9516..0000000000
--- a/dpsim-villas/src/InterfaceSampleBased.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#include <stdexcept>
-#include <cstdio>
-#include <cstdlib>
-
-#include <spdlog/sinks/stdout_color_sinks.h>
-
-#include <dpsim-villas/InterfaceSampleBased.h>
-#include <dpsim-models/Logger.h>
-
-using namespace CPS;
-using namespace DPsim;
-
-void InterfaceSampleBased::close() {
-	mOpened = false;
-}
-
-void InterfaceSampleBased::PreStep::execute(Real time, Int timeStepCount) {
-	if (!mIntf.mImportAttrs.empty()) {
-		if (timeStepCount % mIntf.mDownsampling == 0)
-			mIntf.readValues(mIntf.mSync);
-	}	
-}
-
-void InterfaceSampleBased::PostStep::execute(Real time, Int timeStepCount) {
-	if (!mIntf.mExportAttrs.empty()) {
-		if (timeStepCount % mIntf.mDownsampling == 0)
-			mIntf.writeValues();
-	}
-}
-
-Attribute<Int>::Ptr InterfaceSampleBased::importInt(UInt idx) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return nullptr;
-	}
-	
-	AttributeStatic<Int>::Ptr attr = AttributeStatic<Int>::make();
-	auto& log = mLog;
-	addImport([attr, idx, log](Sample *smp) {
-		if (idx >= smp->length) {
-			log->error("incomplete data received from InterfaceVillas");
-			return;
-		}
-		attr->set(smp->data[idx].i);
-	});
-	mImportAttrs.push_back(attr);
-	mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::INTEGER);
-	return attr;
-}
-
-Attribute<Real>::Ptr InterfaceSampleBased::importReal(UInt idx) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return nullptr;
-	}
-
-	AttributeStatic<Real>::Ptr attr = AttributeStatic<Real>::make();
-	auto& log = mLog;
-	addImport([attr, idx, log](Sample *smp) {
-		if (idx >= smp->length) {
-			log->error("incomplete data received from InterfaceVillas");
-			return;
-		}
-		attr->set(smp->data[idx].f);
-	});
-	mImportAttrs.push_back(attr);
-	mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::FLOAT);
-	return attr;
-}
-
-Attribute<Bool>::Ptr InterfaceSampleBased::importBool(UInt idx) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return nullptr;
-	}
-
-	AttributeStatic<Bool>::Ptr attr = AttributeStatic<Bool>::make();
-	auto& log = mLog;
-	addImport([attr, idx, log](Sample *smp) {
-		if (idx >= smp->length) {
-			log->error("incomplete data received from InterfaceVillas");
-			return;
-		}
-		attr->set(smp->data[idx].b);
-	});
-	mImportAttrs.push_back(attr);
-	mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::BOOLEAN);
-	return attr;
-}
-
-Attribute<Complex>::Ptr InterfaceSampleBased::importComplex(UInt idx) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return nullptr;
-	}
-
-	AttributeStatic<Complex>::Ptr attr = AttributeStatic<Complex>::make();
-	auto& log = mLog;
-	addImport([attr, idx, log](Sample *smp) {
-		if (idx >= smp->length) {
-			log->error("incomplete data received from InterfaceVillas");
-			return;
-		}
-		auto y = Complex(smp->data[idx].z.real(), smp->data[idx].z.imag());
-
-		attr->set(y);
-	});
-	mImportAttrs.push_back(attr);
-	mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::COMPLEX);
-	return attr;
-}
-
-Attribute<Complex>::Ptr InterfaceSampleBased::importComplexMagPhase(UInt idx) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return nullptr;
-	}
-
-	AttributeStatic<Complex>::Ptr attr = AttributeStatic<Complex>::make();
-	auto& log = mLog;
-	addImport([attr, idx, log](Sample *smp) {
-		if (idx >= smp->length) {
-			log->error("incomplete data received from InterfaceVillas");
-			return;
-		}
-		auto *z = reinterpret_cast<float*>(&smp->data[idx].z);
-		auto  y = std::polar(z[0], z[1]);
-
-		attr->set(y);
-	});
-	mImportAttrs.push_back(attr);
-	mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::COMPLEX);
-	return attr;
-}
-
-void InterfaceSampleBased::exportInt(Attribute<Int>::Ptr attr, UInt idx, const std::string &name, const std::string &unit) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return;
-	}
-
-	addExport([attr, idx](Sample *smp) {
-		if (idx >= smp->capacity)
-			throw std::out_of_range("not enough space in allocated sample");
-		if (idx >= smp->length)
-			smp->length = idx + 1;
-
-		smp->data[idx].i = **attr;
-	});
-	mExportAttrs.push_back(attr);
-	mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::INTEGER);
-}
-
-void InterfaceSampleBased::exportReal(Attribute<Real>::Ptr attr, UInt idx, const std::string &name, const std::string &unit) {
-	addExport([attr, idx](Sample *smp) {
-		if (idx >= smp->capacity)
-			throw std::out_of_range("not enough space in allocated sample");
-		if (idx >= smp->length)
-			smp->length = idx + 1;
-
-		smp->data[idx].f = **attr;
-	});
-	mExportAttrs.push_back(attr);
-	mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::FLOAT);
-}
-
-void InterfaceSampleBased::exportBool(Attribute<Bool>::Ptr attr, UInt idx, const std::string &name, const std::string &unit) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return;
-	}
-	
-	addExport([attr, idx](Sample *smp) {
-		if (idx >= smp->capacity)
-			throw std::out_of_range("not enough space in allocated sample");
-		if (idx >= smp->length)
-			smp->length = idx + 1;
-
-		smp->data[idx].b = **attr;
-	});
-	mExportAttrs.push_back(attr);
-	mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::BOOLEAN);
-}
-
-void InterfaceSampleBased::exportComplex(Attribute<Complex>::Ptr attr, UInt idx, const std::string &name, const std::string &unit) {
-	if (mOpened) {
-		mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
-		return;
-	}
-	
-	addExport([attr, idx](Sample *smp) {
-		if (idx >= smp->capacity)
-			throw std::out_of_range("not enough space in allocated sample");
-		if (idx >= smp->length)
-			smp->length = idx + 1;
-
-		auto y = **attr;
-
-		smp->data[idx].z = std::complex<float>(y.real(), y.imag());
-	});
-	mExportAttrs.push_back(attr);
-	mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::COMPLEX);
-}
-
-CPS::Task::List InterfaceSampleBased::getTasks() {
-	return CPS::Task::List({
-		std::make_shared<InterfaceSampleBased::PreStep>(*this),
-		std::make_shared<InterfaceSampleBased::PostStep>(*this)
-	});
-}
\ No newline at end of file
diff --git a/dpsim-villas/src/InterfaceShmem.cpp b/dpsim-villas/src/InterfaceShmem.cpp
deleted file mode 100644
index 84166a753d..0000000000
--- a/dpsim-villas/src/InterfaceShmem.cpp
+++ /dev/null
@@ -1,132 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#include <stdexcept>
-#include <cstdio>
-#include <cstdlib>
-
-#include <spdlog/sinks/stdout_color_sinks.h>
-
-#include <dpsim-villas/InterfaceShmem.h>
-#include <dpsim-models/Logger.h>
-
-using namespace CPS;
-using namespace DPsim;
-
-void InterfaceShmem::open(CPS::Logger::Log log) {
-	mLog = log;
-
-	mLog->info("Opening InterfaceShmem: {} <-> {}", mWName, mRName);
-
-	if (shmem_int_open(mWName.c_str(), mRName.c_str(), &mShmem, &mConf) < 0) {
-		mLog->error("Failed to open/map shared memory object");
-		std::exit(1);
-	}
-
-	mLog->info("Opened InterfaceShmem: {} <-> {}", mWName, mRName);
-
-	mSequence = 0;
-
-	if (shmem_int_alloc(&mShmem, &mLastSample, 1) < 0) {
-		mLog->info("Failed to allocate single sample from pool");
-		close();
-		std::exit(1);
-	}
-
-	mLastSample->sequence = 0;
-	mLastSample->ts.origin.tv_sec = 0;
-	mLastSample->ts.origin.tv_nsec = 0;
-
-	std::memset(&mLastSample->data, 0, mLastSample->capacity * sizeof(float));
-}
-
-void InterfaceShmem::close() {
-	shmem_int_close(&mShmem);
-	InterfaceSampleBased::close();
-}
-
-void InterfaceShmem::readValues(bool blocking) {
-	Sample *sample = nullptr;
-	int ret = 0;
-	try {
-		if (!blocking) {
-			// Check if theres actually data available
-			ret = queue_signalled_available(&mShmem.read.shared->queue);
-			if (ret <= 0)
-				return;
-
-			ret = shmem_int_read(&mShmem, &sample, 1);
-			if (ret == 0)
-				return;
-		}
-		else {
-			while (ret == 0)
-				ret = shmem_int_read(&mShmem, &sample, 1);
-		}
-		if (ret < 0) {
-			mLog->error("Fatal error: failed to read sample from InterfaceShmem");
-			close();
-			std::exit(1);
-		}
-
-		for (auto imp : mImports) {
-			imp(sample);
-		}
-
-		sample_decref(sample);
-	}
-	catch (std::exception& exc) {
-		/* probably won't happen (if the timer expires while we're still reading data,
-		 * we have a bigger problem somewhere else), but nevertheless, make sure that
-		 * we're not leaking memory from the queue pool */
-		if (sample)
-			sample_decref(sample);
-
-		throw exc;
-	}
-}
-
-void InterfaceShmem::writeValues() {
-	Sample *sample = nullptr;
-	Int ret = 0;
-	bool done = false;
-	try {
-		if (shmem_int_alloc(&mShmem, &sample, 1) < 1) {
-			mLog->error("Fatal error: pool underrun in: {} <-> {} at sequence no {}", mWName, mRName, mSequence);
-			close();
-			std::exit(1);
-		}
-
-		for (auto exp : mExports) {
-			exp(sample);
-		}
-
-		sample->sequence = mSequence++;
-		sample->flags |= (int) villas::node::SampleFlags::HAS_DATA;
-		clock_gettime(CLOCK_REALTIME, &sample->ts.origin);
-		done = true;
-
-		do {
-			ret = shmem_int_write(&mShmem, &sample, 1);
-		} while (ret == 0);
-		if (ret < 0)
-			mLog->error("Failed to write samples to InterfaceShmem");
-
-		sample_copy(mLastSample, sample);
-	}
-	catch (std::exception& exc) {
-		/* We need to at least send something, so determine where exactly the
-		 * timer expired and either resend the last successfully sent sample or
-		 * just try to send this one again.
-		 * TODO: can this be handled better? */
-		if (!done)
-			sample = mLastSample;
-
-		while (ret == 0)
-			ret = shmem_int_write(&mShmem, &sample, 1);
-
-		if (ret < 0)
-			mLog->error("Failed to write samples to InterfaceShmem");
-
-		/* Don't throw here, because we managed to send something */
-	}
-}
diff --git a/dpsim-villas/src/InterfaceVillas.cpp b/dpsim-villas/src/InterfaceVillas.cpp
index ee1a2fb3d7..6e1e65a52c 100644
--- a/dpsim-villas/src/InterfaceVillas.cpp
+++ b/dpsim-villas/src/InterfaceVillas.cpp
@@ -1,243 +1,23 @@
 // SPDX-License-Identifier: Apache-2.0
 
-#include <stdexcept>
-#include <cstdio>
-#include <cstdlib>
-
-#include <spdlog/sinks/stdout_color_sinks.h>
-
 #include <dpsim-villas/InterfaceVillas.h>
-#include <dpsim-models/Logger.h>
-#include <villas/signal_list.hpp>
-#include <villas/path.hpp>
-
-using namespace CPS;
-using namespace DPsim;
-
-Bool InterfaceVillas::villasInitialized = false;
-UInt InterfaceVillas::villasAffinity = 0;
-UInt InterfaceVillas::villasPriority = 0;
-UInt InterfaceVillas::villasHugePages = 100;
-
-InterfaceVillas::InterfaceVillas(const String &name, const String &nodeConfig, UInt queueLength, UInt sampleLength, UInt downsampling) :
-	InterfaceSampleBased(name, name, true, downsampling), // Set sync=true for all InterfaceVillas instances
-	mNodeConfig(nodeConfig),
-	mQueueLength(queueLength),
-	mSampleLength(sampleLength)
-	{ }
-
-void InterfaceVillas::open(CPS::Logger::Log log) {
-	mLog = log;
-	mLog->info("Opening InterfaceVillas...");
-
-	if (!InterfaceVillas::villasInitialized) {
-		mLog->info("Initializing Villas...");
-		initVillas();
-		InterfaceVillas::villasInitialized = true;
-	}
-
-	json_error_t error;
-	json_t* config = json_loads(mNodeConfig.c_str(), 0, &error);
-	if (config == nullptr) {
-		throw JsonError(config, error);
-	}
-
-	json_t* nodeType = json_object_get(config, "type");
-	if (nodeType == nullptr) {
-		mLog->error("Error: Node config does not contain type-key!");
-		std::exit(1);
-	}
-	String nodeTypeString = json_string_value(nodeType);
-
-	mNode = node::NodeFactory::make(nodeTypeString);
-
-	int ret = 0;
-	uuid_t fakeSuperNodeUUID;
-	uuid_generate_random(fakeSuperNodeUUID);
-	ret = mNode->parse(config, fakeSuperNodeUUID);
-	if (ret < 0) {
-		mLog->error("Error: Node in InterfaceVillas failed to parse config. Parse returned code {}", ret);
-		std::exit(1);
-	}
-	ret = mNode->check();
-	if (ret < 0) {
-		mLog->error("Error: Node in InterfaceVillas failed check. Check returned code {}", ret);
-		std::exit(1);
-	}
-
-	mLog->info("Preparing VILLASNode instance...");
-	setupNodeSignals();
-	prepareNode();
-	mLog->info("Node is ready to send / receive data!");
-	mOpened = true;
-
-	mSequence = 0;
-	mLastSample = node::sample_alloc(&mSamplePool);
-	mLastSample->signals = mNode->getInputSignals(false);
-	mLastSample->sequence = 0;
-	mLastSample->ts.origin.tv_sec = 0;
-	mLastSample->ts.origin.tv_nsec = 0;
-
-	std::memset(&mLastSample->data, 0, mLastSample->capacity * sizeof(float));
-}
-
-
-void InterfaceVillas::prepareNode() {
-	int ret = node::pool_init(&mSamplePool, mQueueLength, sizeof(Sample) + SAMPLE_DATA_LENGTH(mSampleLength));
-	if (ret < 0) {
-		mLog->error("Error: InterfaceVillas failed to init sample pool. pool_init returned code {}", ret);
-		std::exit(1);
-	}
-
-	ret = mNode->prepare();
-	if (ret < 0) {
-		mLog->error("Error: Node in InterfaceVillas failed to prepare. Prepare returned code {}", ret);
-		std::exit(1);
-	}
-
-	mNode->getFactory()->start(nullptr); //We have no SuperNode, so just hope type_start doesnt use it...
-
-	ret = mNode->start();
-	if (ret < 0) {
-		mLog->error("Fatal error: failed to start node in InterfaceVillas. Start returned code {}", ret);
-		close();
-		std::exit(1);
-	}
-}
-
-void InterfaceVillas::setupNodeSignals() {
-	mNode->out.path = new node::Path();
-	mNode->out.path->signals = std::make_shared<node::SignalList>();
-	node::SignalList::Ptr nodeOutputSignals = mNode->out.path->getOutputSignals(false);
-	nodeOutputSignals->clear();
-	int idx = 0;
-	for (auto sig : mExportSignals) {
-		while (sig.first > idx) {
-			nodeOutputSignals->push_back(std::make_shared<node::Signal>("", "", node::SignalType::INVALID));
-			idx++;
-		}
-		nodeOutputSignals->push_back(sig.second);
-		idx++;
-	}
-
-	node::SignalList::Ptr nodeInputSignals = mNode->getInputSignals(false);
-	nodeInputSignals->clear();
-	idx = 0;
-	for (auto sig : mImportSignals) {
-		while (sig.first > idx) {
-			nodeInputSignals->push_back(std::make_shared<node::Signal>("", "", node::SignalType::INVALID));
-			idx++;
-		}
-		nodeInputSignals->push_back(sig.second);
-		idx++;
-	}
-}
-
-void InterfaceVillas::close() {
-	mLog->info("Closing InterfaceVillas...");
-	int ret = mNode->stop();
-	if (ret < 0) {
-		mLog->error("Error: failed to stop node in InterfaceVillas. Stop returned code {}", ret);
-		std::exit(1);
-	}
-	mOpened = false;
-	ret = node::pool_destroy(&mSamplePool);
-	if (ret < 0) {
-		mLog->error("Error: failed to destroy SamplePool in InterfaceVillas. pool_destroy returned code {}", ret);
-		std::exit(1);
-	}
-
-	mNode->getFactory()->stop();
-
-	delete mNode;
-}
-
-void InterfaceVillas::readValues(bool blocking) {
-	Sample *sample = node::sample_alloc(&mSamplePool);
-	int ret = 0;
-	try {
-		while (ret == 0)
-			ret = mNode->read(&sample, 1);
-		if (ret < 0) {
-			mLog->error("Fatal error: failed to read sample from InterfaceVillas. Read returned code {}", ret);
-			close();
-			std::exit(1);
-		}
-
-		for (auto imp : mImports) {
-			imp(sample);
-		}
-
-		sample_decref(sample);
-	}
-	catch (std::exception& exc) {
-		/* probably won't happen (if the timer expires while we're still reading data,
-		 * we have a bigger problem somewhere else), but nevertheless, make sure that
-		 * we're not leaking memory from the queue pool */
-		if (sample)
-			sample_decref(sample);
-
-		throw exc;
-	}
-}
-
-void InterfaceVillas::writeValues() {
-	Sample *sample = nullptr;
-	Int ret = 0;
-	bool done = false;
-	try {
-		sample = node::sample_alloc(&mSamplePool);
-		if (sample == nullptr) {
-			mLog->error("InterfaceVillas could not allocate a new sample! Not sending any data!");
-			return;
-		}
-
-		sample->signals = mNode->getOutputSignals(false);
-
-		for (auto exp : mExports) {
-			exp(sample);
-		}
-
-		sample->sequence = mSequence++;
-		sample->flags |= (int) villas::node::SampleFlags::HAS_SEQUENCE;
-		sample->flags |= (int) villas::node::SampleFlags::HAS_DATA;
-		clock_gettime(CLOCK_REALTIME, &sample->ts.origin);
-		sample->flags |= (int) villas::node::SampleFlags::HAS_TS_ORIGIN;
-		done = true;
-
-		do {
-			ret = mNode->write(&sample, 1);
-		} while (ret == 0);
-		if (ret < 0)
-			mLog->error("Failed to write samples to InterfaceVillas. Write returned code {}", ret);
-
-		sample_copy(mLastSample, sample);
-		sample_decref(sample);
-	}
-	catch (std::exception& exc) {
-		/* We need to at least send something, so determine where exactly the
-		 * timer expired and either resend the last successfully sent sample or
-		 * just try to send this one again.
-		 * TODO: can this be handled better? */
-		if (!done)
-			sample = mLastSample;
+#include <dpsim-villas/InterfaceWorkerVillas.h>
 
-		while (ret == 0)
-			ret = mNode->write(&sample, 1);
+using namespace villas;
 
-		sample_decref(sample);
+namespace DPsim {
 
-		if (ret < 0)
-			mLog->error("Failed to write samples to InterfaceVillas. Write returned code {}", ret);
+    InterfaceVillas::InterfaceVillas(const String &nodeConfig, UInt queueLength, UInt sampleLength, const String& name, UInt downsampling)
+        : Interface(InterfaceWorkerVillas::make(nodeConfig, queueLength, sampleLength), name, downsampling) { }
 
-		/* Don't throw here, because we managed to send something */
-	}
-}
+    void InterfaceVillas::importAttribute(CPS::AttributeBase::Ptr attr, UInt idx, Bool blockOnRead, Bool syncOnSimulationStart) {
+        Interface::addImport(attr, blockOnRead, syncOnSimulationStart);
+        std::dynamic_pointer_cast<InterfaceWorkerVillas>(mInterfaceWorker)->configureImport((UInt)mImportAttrsDpsim.size() - 1, attr->getType(), idx);
+    }
 
-void InterfaceVillas::initVillas() {
-	int ret = node::memory::init(villasHugePages);
-	if (ret)
-		throw RuntimeError("Error: VillasNode failed to initialize memory system");
+	void InterfaceVillas::exportAttribute(CPS::AttributeBase::Ptr attr, UInt idx, Bool waitForOnWrite, const String& name, const String& unit) {
+        Interface::addExport(attr);
+        std::dynamic_pointer_cast<InterfaceWorkerVillas>(mInterfaceWorker)->configureExport((UInt)mExportAttrsDpsim.size() - 1, attr->getType(), idx, waitForOnWrite, name, unit);
+    }
 
-	villas::kernel::rt::init(villasPriority, villasAffinity);
 }
\ No newline at end of file
diff --git a/dpsim-villas/src/InterfaceWorkerVillas.cpp b/dpsim-villas/src/InterfaceWorkerVillas.cpp
new file mode 100644
index 0000000000..516ceb75d8
--- /dev/null
+++ b/dpsim-villas/src/InterfaceWorkerVillas.cpp
@@ -0,0 +1,484 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <stdexcept>
+#include <cstdio>
+#include <cstdlib>
+
+#include <unistd.h>
+#include <poll.h>
+#include <chrono>
+#include <thread>
+
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+#include <dpsim-villas/InterfaceWorkerVillas.h>
+#include <dpsim-models/Logger.h>
+#include <villas/signal_list.hpp>
+#include <villas/path.hpp>
+
+using namespace CPS;
+using namespace DPsim;
+
+Bool InterfaceWorkerVillas::villasInitialized = false;
+UInt InterfaceWorkerVillas::villasAffinity = 0;
+UInt InterfaceWorkerVillas::villasPriority = 0;
+UInt InterfaceWorkerVillas::villasHugePages = 100;
+
+InterfaceWorkerVillas::InterfaceWorkerVillas(const String &nodeConfig, UInt queueLength, UInt sampleLength) :
+	InterfaceWorker(),
+	mNodeConfig(nodeConfig),
+	mQueueLength(queueLength),
+	mSampleLength(sampleLength)
+	{ }
+
+void InterfaceWorkerVillas::open() {
+	mLog->info("Opening InterfaceWorkerVillas...");
+
+	if (!InterfaceWorkerVillas::villasInitialized) {
+		mLog->info("Initializing Villas...");
+		initVillas();
+		InterfaceWorkerVillas::villasInitialized = true;
+	}
+
+	json_error_t error;
+	json_t* config = json_loads(mNodeConfig.c_str(), 0, &error);
+	if (config == nullptr) {
+		throw JsonError(config, error);
+	}
+
+	const json_t* nodeType = json_object_get(config, "type");
+	if (nodeType == nullptr) {
+		mLog->error("Error: Node config does not contain type-key!");
+		std::exit(1);
+	}
+	String nodeTypeString = json_string_value(nodeType);
+
+	mNode = node::NodeFactory::make(nodeTypeString);
+
+	int ret = 0;
+	uuid_t fakeSuperNodeUUID;
+	uuid_generate_random(fakeSuperNodeUUID);
+	ret = mNode->parse(config, fakeSuperNodeUUID);
+	if (ret < 0) {
+		mLog->error("Error: Node in InterfaceVillas failed to parse config. Parse returned code {}", ret);
+		std::exit(1);
+	}
+	ret = mNode->check();
+	if (ret < 0) {
+		mLog->error("Error: Node in InterfaceVillas failed check. Check returned code {}", ret);
+		std::exit(1);
+	}
+
+	mLog->info("Preparing VILLASNode instance...");
+	setupNodeSignals();
+	prepareNode();
+	mLog->info("Node is ready to send / receive data!");
+	mOpened = true;
+
+	mSequence = 0;
+	mLastSample = node::sample_alloc(&mSamplePool);
+	mLastSample->signals = mNode->getInputSignals(false);
+	mLastSample->sequence = 0;
+	mLastSample->ts.origin.tv_sec = 0;
+	mLastSample->ts.origin.tv_nsec = 0;
+
+	std::memset(&mLastSample->data, 0, mLastSample->capacity * sizeof(float));
+}
+
+
+void InterfaceWorkerVillas::prepareNode() {
+	int ret = node::pool_init(&mSamplePool, mQueueLength, sizeof(Sample) + SAMPLE_DATA_LENGTH(mSampleLength));
+	if (ret < 0) {
+		mLog->error("Error: InterfaceVillas failed to init sample pool. pool_init returned code {}", ret);
+		std::exit(1);
+	}
+
+	ret = mNode->prepare();
+	if (ret < 0) {
+		mLog->error("Error: Node in InterfaceVillas failed to prepare. Prepare returned code {}", ret);
+		std::exit(1);
+	}
+
+	mNode->getFactory()->start(nullptr); //We have no SuperNode, so just hope type_start doesnt use it...
+
+	ret = mNode->start();
+	if (ret < 0) {
+		mLog->error("Fatal error: failed to start node in InterfaceVillas. Start returned code {}", ret);
+		close();
+		std::exit(1);
+	}
+}
+
+void InterfaceWorkerVillas::setupNodeSignals() {
+	mNode->out.path = new node::Path();
+	mNode->out.path->signals = std::make_shared<node::SignalList>();
+	node::SignalList::Ptr nodeOutputSignals = mNode->out.path->getOutputSignals(false);
+	nodeOutputSignals->clear();
+	int idx = 0;
+	for (const auto& [id, signal] : mExportSignals) {
+		while (id > idx) {
+			nodeOutputSignals->push_back(std::make_shared<node::Signal>("", "", node::SignalType::INVALID));
+			idx++;
+		}
+		nodeOutputSignals->push_back(signal);
+		idx++;
+	}
+
+	node::SignalList::Ptr nodeInputSignals = mNode->getInputSignals(false);
+	nodeInputSignals->clear();
+	idx = 0;
+	for (const auto& [id, signal] : mImportSignals) {
+		while (id > idx) {
+			nodeInputSignals->push_back(std::make_shared<node::Signal>("", "", node::SignalType::INVALID));
+			idx++;
+		}
+		nodeInputSignals->push_back(signal);
+		idx++;
+	}
+}
+
+void InterfaceWorkerVillas::close() {
+	mLog->info("Closing InterfaceVillas...");
+	int ret = mNode->stop();
+	if (ret < 0) {
+		mLog->error("Error: failed to stop node in InterfaceVillas. Stop returned code {}", ret);
+		std::exit(1);
+	}
+	mOpened = false;
+	ret = node::pool_destroy(&mSamplePool);
+	if (ret < 0) {
+		mLog->error("Error: failed to destroy SamplePool in InterfaceVillas. pool_destroy returned code {}", ret);
+		std::exit(1);
+	}
+
+	mNode->getFactory()->stop();
+
+	delete mNode;
+}
+
+void InterfaceWorkerVillas::readValuesFromEnv(std::vector<Interface::AttributePacket>& updatedAttrs) {
+	Sample *sample = nullptr;
+	int ret = 0;
+	bool shouldRead = false;
+	try {
+		auto pollFds = mNode->getPollFDs();
+
+		if (!pollFds.empty()) {
+			auto pfds = std::vector<struct pollfd>();
+
+			for (auto pollFd : pollFds) {
+				pfds.push_back(pollfd {
+					.fd = pollFd,
+					.events = POLLIN
+				});
+			}
+
+			ret = ::poll(pfds.data(), pfds.size(), 0);
+
+			if (ret < 0) {
+				mLog->error("Fatal error: failed to read sample from InterfaceVillas. Poll returned code {}", ret);
+				close();
+				std::exit(1);
+			}
+
+			if (ret == 0){
+				return;
+			}
+
+			for (const auto &pfd : pfds) {
+				if (pfd.revents & POLLIN) {
+					shouldRead = true;
+					break;
+				}
+			}
+		} else  {
+			//If the node does not support pollFds just do a blocking read
+			shouldRead = true;
+		}
+
+		if (shouldRead) {
+			sample = node::sample_alloc(&mSamplePool);
+
+			ret = mNode->read(&sample, 1);
+			if (ret < 0) {
+				mLog->error("Fatal error: failed to read sample from InterfaceVillas. Read returned code {}", ret);
+				close();
+				std::exit(1);
+			}
+
+			if (ret != 0) {
+				for (UInt i = 0; i < mImports.size(); i++) {
+					auto importedAttr = std::get<0>(mImports[i])(sample);
+					if (!importedAttr.isNull()) {
+						updatedAttrs.emplace_back(Interface::AttributePacket {
+							importedAttr,
+							i,
+							mCurrentSequenceInterfaceToDpsim,
+							Interface::AttributePacketFlags::PACKET_NO_FLAGS
+						});
+						mCurrentSequenceInterfaceToDpsim++;
+					}
+				}
+
+				if (!pollFds.empty()) {
+					//Manually clear the event file descriptor since Villas does not do that for some reason
+					//See https://git.rwth-aachen.de/acs/public/villas/node/-/issues/347
+					uint64_t result = 0;
+					ret = (int) ::read(pollFds[0], &result, 8);
+					if (ret < 0) {
+						mLog->warn("Could not reset poll file descriptor! Read returned {}", ret);
+					}
+					if (result > 1) {
+						result = result - 1;
+						ret = (int) ::write(pollFds[0], (void*) &result, 8);
+						if (ret < 0) {
+							mLog->warn("Could not decrement poll file descriptor! Write returned {}", ret);
+						}
+					}
+				}
+
+			}
+
+			sample_decref(sample);
+		}
+	}
+	catch (const std::exception&) {
+		if (sample)
+			sample_decref(sample);
+
+		throw;
+	}
+}
+
+void InterfaceWorkerVillas::writeValuesToEnv(std::vector<Interface::AttributePacket>& updatedAttrs) {
+	
+	//Update export sequence IDs
+	for (const auto& packet : updatedAttrs) {
+		if (std::get<1>(mExports[packet.attributeId]) < packet.sequenceId) {
+			std::get<1>(mExports[packet.attributeId]) = packet.sequenceId;
+		}
+	}
+
+	//Remove outdated packets
+	auto beginOutdated = std::remove_if(updatedAttrs.begin(), updatedAttrs.end(), [this](auto packet) {
+		return std::get<1>(mExports[packet.attributeId]) > packet.sequenceId;
+	});
+	updatedAttrs.erase(beginOutdated, updatedAttrs.end());
+	
+	Sample *sample = nullptr;
+	Int ret = 0;
+	bool done = false;
+	bool sampleFilled = false;
+	try {
+		sample = node::sample_alloc(&mSamplePool);
+		if (sample == nullptr) {
+			mLog->error("InterfaceVillas could not allocate a new sample! Not sending any data!");
+			return;
+		}
+
+		sample->signals = mNode->getOutputSignals(false);
+		auto beginExported = std::remove_if(updatedAttrs.begin(), updatedAttrs.end(), [this, &sampleFilled, &sample](auto packet) {
+			if (!std::get<2>(mExports[packet.attributeId])) {
+				//Write attribute to sample ASAP
+				std::get<0>(mExports[packet.attributeId])(packet.value, sample);
+				sampleFilled = true;
+				return true;
+			}
+			return false;
+		});
+		updatedAttrs.erase(beginExported, updatedAttrs.end());
+
+		//Check if the remaining packets form a complete set
+		if (((long) updatedAttrs.size()) == std::count_if(mExports.cbegin(), mExports.cend(), [this](auto x) {
+			return std::get<2>(x);
+		})) {
+			for (const auto& packet : updatedAttrs) {
+				std::get<0>(mExports[packet.attributeId])(packet.value, sample);
+			}
+			sampleFilled = true;
+			updatedAttrs.clear();
+		}
+
+		if (sampleFilled) {
+			sample->sequence = mSequence++;
+			sample->flags |= (int) villas::node::SampleFlags::HAS_SEQUENCE;
+			sample->flags |= (int) villas::node::SampleFlags::HAS_DATA;
+			clock_gettime(CLOCK_REALTIME, &sample->ts.origin);
+			sample->flags |= (int) villas::node::SampleFlags::HAS_TS_ORIGIN;
+			done = true;
+
+			do {
+				ret = mNode->write(&sample, 1);
+			} while (ret == 0);
+			if (ret < 0)
+				mLog->error("Failed to write samples to InterfaceVillas. Write returned code {}", ret);
+
+			sample_copy(mLastSample, sample);
+		}
+		sample_decref(sample);
+	}
+	catch (const std::exception&) {
+		/* We need to at least send something, so determine where exactly the
+		 * timer expired and either resend the last successfully sent sample or
+		 * just try to send this one again.
+		 * TODO: can this be handled better? */
+		if (!done)
+			sample = mLastSample;
+
+		while (ret == 0)
+			ret = mNode->write(&sample, 1);
+
+		sample_decref(sample);
+
+		if (ret < 0)
+			mLog->error("Failed to write samples to InterfaceVillas. Write returned code {}", ret);
+
+		/* Don't throw here, because we managed to send something */
+	}
+}
+
+void InterfaceWorkerVillas::initVillas() const {
+	if (int ret = node::memory::init(villasHugePages); ret)
+		throw RuntimeError("Error: VillasNode failed to initialize memory system");
+
+	villas::kernel::rt::init(villasPriority, villasAffinity);
+}
+
+void InterfaceWorkerVillas::configureExport(UInt attributeId, const std::type_info& type, UInt idx, Bool waitForOnWrite, const String& name, const String& unit) {
+	if (mOpened) {
+		if (mLog != nullptr) {
+			mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
+		}
+		return;
+	}
+	if (attributeId != mExports.size()) {
+		if (mLog != nullptr) {
+			mLog->warn("The exports already configured do not match with the given attribute ID! Configuration will remain unchanged.");
+		}
+		return; 
+	}
+
+	if (type == typeid(Int)) {
+		mExports.emplace_back([idx](AttributeBase::Ptr attr, Sample *smp) {
+			if (idx >= smp->capacity)
+				throw std::out_of_range("not enough space in allocated sample");
+			if (idx >= smp->length)
+				smp->length = idx + 1;
+
+			Attribute<Int>::Ptr attrTyped = std::dynamic_pointer_cast<Attribute<Int>>(attr.getPtr());
+
+			if (attrTyped.isNull())
+				throw InvalidAttributeException();
+
+			smp->data[idx].i = **attrTyped;
+		}, 0, waitForOnWrite);
+		mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::INTEGER);
+	} else if (type == typeid(Real)) {
+		mExports.emplace_back([idx](AttributeBase::Ptr attr, Sample *smp) {
+			if (idx >= smp->capacity)
+				throw std::out_of_range("not enough space in allocated sample");
+			if (idx >= smp->length)
+				smp->length = idx + 1;
+
+			Attribute<Real>::Ptr attrTyped = std::dynamic_pointer_cast<Attribute<Real>>(attr.getPtr());
+
+			if (attrTyped.isNull())
+				throw InvalidAttributeException();
+
+			smp->data[idx].f = **attrTyped;
+		}, 0, waitForOnWrite);
+		mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::FLOAT);
+	} else if (type == typeid(Complex)) {
+		mExports.emplace_back([idx](AttributeBase::Ptr attr, Sample *smp) {
+			if (idx >= smp->capacity)
+				throw std::out_of_range("not enough space in allocated sample");
+			if (idx >= smp->length)
+				smp->length = idx + 1;
+
+			Attribute<Complex>::Ptr attrTyped = std::dynamic_pointer_cast<Attribute<Complex>>(attr.getPtr());
+
+			if (attrTyped.isNull())
+				throw InvalidAttributeException();
+
+			smp->data[idx].z = **attrTyped;
+		}, 0, waitForOnWrite);
+		mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::COMPLEX);
+	} else if (type == typeid(Bool)) {
+		mExports.emplace_back([idx](AttributeBase::Ptr attr, Sample *smp) {
+			if (idx >= smp->capacity)
+				throw std::out_of_range("not enough space in allocated sample");
+			if (idx >= smp->length)
+				smp->length = idx + 1;
+
+			Attribute<Bool>::Ptr attrTyped = std::dynamic_pointer_cast<Attribute<Bool>>(attr.getPtr());
+
+			if (attrTyped.isNull())
+				throw InvalidAttributeException();
+
+			smp->data[idx].b = **attrTyped;
+		}, 0, waitForOnWrite);
+		mExportSignals[idx] = std::make_shared<node::Signal>(name, unit, node::SignalType::BOOLEAN);
+	} else {
+		if (mLog != nullptr) {
+			mLog->warn("Unsupported attribute type! Interface configuration will remain unchanged!");
+		}
+	}	
+}
+
+void InterfaceWorkerVillas::configureImport(UInt attributeId, const std::type_info& type, UInt idx) {
+	if (mOpened) {
+		if (mLog != nullptr) {
+			mLog->warn("InterfaceVillas has already been opened! Configuration will remain unchanged.");
+		}
+		return;
+	}
+	if (attributeId != mImports.size()) {
+		if (mLog != nullptr) {
+			mLog->warn("The imports already configured do not match with the given attribute ID! Configuration will remain unchanged.");
+		}
+		return; 
+	}
+	const auto& log = mLog;
+
+	if (type == typeid(Int)) {
+		mImports.emplace_back([idx, log](Sample *smp) -> AttributeBase::Ptr {
+			if (idx >= smp->length) {
+				log->error("incomplete data received from InterfaceVillas");
+				return nullptr;
+			}
+			return AttributePointer<AttributeBase>(AttributeStatic<Int>::make(smp->data[idx].i));
+		}, 0);
+		mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::INTEGER);
+	} else if (type == typeid(Real)) {
+		mImports.emplace_back([idx, log](Sample *smp) -> AttributeBase::Ptr {
+			if (idx >= smp->length) {
+				log->error("incomplete data received from InterfaceVillas");
+				return nullptr;
+			}
+			return AttributePointer<AttributeBase>(AttributeStatic<Real>::make(smp->data[idx].f));
+		}, 0);
+		mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::FLOAT);
+	} else if (type == typeid(Complex)) {
+		mImports.emplace_back([idx, log](Sample *smp) -> AttributeBase::Ptr {
+			if (idx >= smp->length) {
+				log->error("incomplete data received from InterfaceVillas");
+				return nullptr;
+			}
+			return AttributePointer<AttributeBase>(AttributeStatic<Complex>::make(smp->data[idx].z));
+		}, 0);
+		mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::COMPLEX);
+	} else if (type == typeid(Bool)) {
+		mImports.emplace_back([idx, log](Sample *smp) -> AttributeBase::Ptr {
+			if (idx >= smp->length) {
+				log->error("incomplete data received from InterfaceVillas");
+				return nullptr;
+			}
+			return AttributePointer<AttributeBase>(AttributeStatic<Bool>::make(smp->data[idx].b));
+		}, 0);
+		mImportSignals[idx] = std::make_shared<node::Signal>("", "", node::SignalType::BOOLEAN);
+	} else {
+		if (mLog != nullptr) {
+			mLog->warn("Unsupported attribute type! Interface configuration will remain unchanged!");
+		}
+	}	
+}
\ No newline at end of file
diff --git a/dpsim-villas/src/pybind-dpsim-villas.cpp b/dpsim-villas/src/pybind-dpsim-villas.cpp
index 0bdb1eb371..b747d90c43 100644
--- a/dpsim-villas/src/pybind-dpsim-villas.cpp
+++ b/dpsim-villas/src/pybind-dpsim-villas.cpp
@@ -4,74 +4,25 @@
 #include <pybind11/complex.h>
 #include <pybind11/stl.h>
 
-#include <dpsim-villas/InterfaceShmem.h>
 #include <dpsim-villas/InterfaceVillas.h>
 
+PYBIND11_DECLARE_HOLDER_TYPE(T, CPS::AttributePointer<T>);
+
 namespace py = pybind11;
 using namespace py::literals;
 using namespace villas;
 
-class PyInterfaceShmem : public DPsim::InterfaceShmem {
-
-public:
-	using DPsim::InterfaceShmem::InterfaceShmem;
-
-	py::dict getConfig() {
-		auto signals = std::list<py::dict>();
-
-		int maxIdx = 0;
-		for (auto const& a : mExportSignals) {
-			if (a.first > maxIdx)
-				maxIdx = a.first;
-		}
-
-		for (int i = 0; i <= maxIdx; i++) {
-			node::Signal::Ptr s;
-			try {
-				s = mExportSignals.at(i);
-			} catch(std::out_of_range &) {
-				s = std::make_shared<node::Signal>("", "", node::SignalType::FLOAT);
-			}
-
-			auto signal = py::dict(
-				"name"_a = s->name,
-				"type"_a = node::signalTypeToString(s->type)
-			);
-
-			if (!s->unit.empty()) {
-				signal["unit"] = s->unit;
-			}
-
-			signals.push_back(signal);
-		}
-
-		return py::dict(
-			"type"_a = "shmem",
-			"queuelen"_a = mConf.queuelen,
-			"samplelen"_a = mConf.samplelen,
-			"mode"_a = mConf.polling ? "polling" : "pthread",
-			"in"_a = py::dict(
-				"name"_a = mWName,
-				"signals"_a = signals
-			),
-			"out"_a = py::dict(
-				"name"_a = mRName
-			)
-		);
-	}
-};
-
 class PyInterfaceVillas: public DPsim::InterfaceVillas {
 
 public:
 	using DPsim::InterfaceVillas::InterfaceVillas;
 
-	PyInterfaceVillas(const CPS::String &name, py::dict config, CPS::UInt queueLength, CPS::UInt sampleLength, CPS::UInt downsampling) :
-		InterfaceVillas(
-			name, 
+	PyInterfaceVillas(py::dict config, CPS::UInt queueLength, CPS::UInt sampleLength, const CPS::String &name, CPS::UInt downsampling) :
+		InterfaceVillas( 
 			(py::str) py::module_::import("json").attr("dumps")(config, "indent"_a = py::none()), //json.dumps(config, indent=None)
 			queueLength,
 			sampleLength,
+			name,
 			downsampling)
 		{}
 };
@@ -81,11 +32,9 @@ class PyInterfaceVillas: public DPsim::InterfaceVillas {
 PYBIND11_MODULE(dpsimpyvillas, m) {
 	py::object interface = (py::object) py::module_::import("dpsimpy").attr("Interface");
 
-	py::class_<PyInterfaceShmem>(m, "InterfaceShmem", interface)
-	    .def(py::init<const CPS::String&, const CPS::String&>(), py::arg("shmwrite") = "/dpsim-villas", py::arg("shmread") = "/villas-dpsim")
-		.def("get_config", &PyInterfaceShmem::getConfig);
-
-	py::class_<PyInterfaceVillas>(m, "InterfaceVillas", interface)
-	    .def(py::init<const CPS::String&, const CPS::String&, CPS::UInt, CPS::UInt, CPS::UInt>(), "name"_a, "config"_a, "queue_length"_a=512, "sample_length"_a = 64, "downsampling"_a=1)
-		.def(py::init<const CPS::String&, py::dict, CPS::UInt, CPS::UInt, CPS::UInt>(), "name"_a, "config"_a, "queue_length"_a=512, "sample_length"_a = 64, "downsampling"_a=1);
+	py::class_<PyInterfaceVillas, std::shared_ptr<PyInterfaceVillas>>(m, "InterfaceVillas", interface)
+	    .def(py::init<const CPS::String&, CPS::UInt, CPS::UInt, const CPS::String&, CPS::UInt>(), "config"_a, "queue_length"_a=512, "sample_length"_a = 64, "name"_a = "", "downsampling"_a=1) // cppcheck-suppress assignBoolToPointer
+		.def(py::init<py::dict, CPS::UInt, CPS::UInt, const CPS::String&, CPS::UInt>(), "config"_a, "queue_length"_a=512, "sample_length"_a = 64, "name"_a = "", "downsampling"_a=1) // cppcheck-suppress assignBoolToPointer
+		.def("import_attribute", &PyInterfaceVillas::importAttribute, "attr"_a, "idx"_a, "block_on_read"_a = false, "sync_on_start"_a = true) // cppcheck-suppress assignBoolToPointer
+		.def("export_attribute", &PyInterfaceVillas::exportAttribute, "attr"_a, "idx"_a, "wait_for_on_write"_a = true, "name"_a = "", "unit"_a = ""); // cppcheck-suppress assignBoolToPointer
 }
diff --git a/dpsim/include/dpsim/Interface.h b/dpsim/include/dpsim/Interface.h
index 21c7889612..1c59d863c9 100644
--- a/dpsim/include/dpsim/Interface.h
+++ b/dpsim/include/dpsim/Interface.h
@@ -1,64 +1,163 @@
-/* Copyright 2017-2021 Institute for Automation of Complex Power Systems,
- *                     EONERC, RWTH Aachen University
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- *********************************************************************************/
+// SPDX-License-Identifier: Apache-2.0
 
 #pragma once
 
+#include <thread>
+
 #include <dpsim-models/Logger.h>
 #include <dpsim/Config.h>
 #include <dpsim/Definitions.h>
 #include <dpsim/Scheduler.h>
+#include <dpsim/Interface.h>
 #include <dpsim-models/Attribute.h>
 #include <dpsim-models/Task.h>
 
+#include <readerwriterqueue.h>
+
 namespace DPsim {
 
-	/**
-	 * After an Interface is created, components that should use values
-	 * from this interface can be registered with it using the appropiate
-	 * methods implemented by the subclass. Subclasses must also implement the
-	 * readValues and writeValues methods, which should update the values of
-	 * the registered components or send voltages or currents to the external
-	 * sink.
-	 */
-	class Interface {
+	class InterfaceWorker;
+
+	class Interface :
+		public SharedFactory<Interface> {
 
 	public:
 		typedef std::shared_ptr<Interface> Ptr;
 
-        Interface() = default;
-		virtual ~Interface() { };
+		using AttributePacket = struct AttributePacket {
+			CPS::AttributeBase::Ptr value;
+			UInt attributeId; //Used to identify the attribute. Defined by the position in the `mExportAttrsDpsim` and `mImportAttrsDpsim` lists
+			UInt sequenceId; //Increasing ID used to discern multiple consecutive updates of a single attribute
+			unsigned char flags; //Bit 0 set: Close interface
+		};
+
+		enum AttributePacketFlags {
+			PACKET_NO_FLAGS = 0,
+			PACKET_CLOSE_INTERFACE = 1,
+		};
+
+        Interface(std::shared_ptr<InterfaceWorker> intf, const String& name = "", UInt downsampling = 1) : 
+			mInterfaceWorker(intf),
+			mName(name),
+			mDownsampling(downsampling) {
+				mQueueDpsimToInterface = std::make_shared<moodycamel::BlockingReaderWriterQueue<AttributePacket>>();
+				mQueueInterfaceToDpsim = std::make_shared<moodycamel::BlockingReaderWriterQueue<AttributePacket>>();
+			};
+
+		virtual void open();
+		virtual void close();
+
+		// Function used in the interface's simulation task to read all imported attributes from the queue
+		// Called once before every simulation timestep
+		virtual void pushDpsimAttrsToQueue();
+		// Function used in the interface's simulation task to write all exported attributes to the queue
+		// Called once after every simulation timestep
+		virtual void popDpsimAttrsFromQueue(bool isSync = false);
+
+		// Function called by the Simulation to perform interface synchronization
+		virtual void syncExports();
+		/// Function called by the Simulation to perform interface synchronization
+		virtual void syncImports();
+
+		virtual CPS::Task::List getTasks();
+
+		void setLogger(CPS::Logger::Log log);
+
+		virtual ~Interface() {
+			if (mOpened)
+				close();
+		}
+
+		// Attributes used in the DPsim simulation. Should only be accessed by the dpsim-thread
+		// Tuple attributes: Attribute to be imported, Current sequenceID, blockOnRead, syncOnSimulationStart
+		std::vector<std::tuple<CPS::AttributeBase::Ptr, UInt, bool, bool>> mImportAttrsDpsim;
+		// Tuple attributes: Attribute to be exported, Current Sequence ID
+		std::vector<std::tuple<CPS::AttributeBase::Ptr, UInt>> mExportAttrsDpsim;
+
+	protected:
+		std::shared_ptr<InterfaceWorker> mInterfaceWorker;
+		CPS::Logger::Log mLog;
+		String mName;
+		bool mSyncOnSimulationStart;
+		UInt mCurrentSequenceDpsimToInterface = 1;
+		UInt mNextSequenceInterfaceToDpsim = 1;
+		UInt mDownsampling;
+		std::atomic<bool> mOpened;
+		std::thread mInterfaceWriterThread;
+		std::thread mInterfaceReaderThread;
+
+		std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> mQueueDpsimToInterface;
+		std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> mQueueInterfaceToDpsim;
+
+		virtual void addImport(CPS::AttributeBase::Ptr attr, bool blockOnRead = false, bool syncOnSimulationStart = true);
+		virtual void addExport(CPS::AttributeBase::Ptr attr);
+
+	public:
+
+		class WriterThread {
+			private:
+				std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> mQueueDpsimToInterface;
+				std::shared_ptr<InterfaceWorker> mInterfaceWorker;
+
+			public:
+				WriterThread(
+						std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> queueDpsimToInterface,
+				 		std::shared_ptr<InterfaceWorker> intf
+					) :
+					mQueueDpsimToInterface(queueDpsimToInterface),
+					mInterfaceWorker(intf) {};
+				void operator() () const;
+		};
+ 
+		class ReaderThread {
+			private:
+				std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> mQueueInterfaceToDpsim;
+				std::shared_ptr<InterfaceWorker> mInterfaceWorker;
+				std::atomic<bool>& mOpened;
+
+			public:
+				ReaderThread(
+						std::shared_ptr<moodycamel::BlockingReaderWriterQueue<AttributePacket>> queueInterfaceToDpsim,
+				 		std::shared_ptr<InterfaceWorker> intf,
+						std::atomic<bool>& opened
+					) :
+					mQueueInterfaceToDpsim(queueInterfaceToDpsim),
+					mInterfaceWorker(intf),
+					mOpened(opened) {};
+				void operator() () const;
+		};
+
+		class PreStep : public CPS::Task {
+		public:
+			explicit PreStep(Interface& intf) :
+				Task(intf.mName + ".Read"), mIntf(intf) {
+				for (const auto& [attr, _seqId, _blockOnRead, _syncOnStart] : intf.mImportAttrsDpsim) {
+					mModifiedAttributes.push_back(attr);
+				}
+			}
 
-		virtual void open(CPS::Logger::Log log) = 0;
-		virtual void close() = 0;
+			void execute(Real time, Int timeStepCount) override;
 
-		virtual CPS::Attribute<Int>::Ptr importInt(UInt idx) = 0;
-		virtual CPS::Attribute<Real>::Ptr importReal(UInt idx) = 0;
-		virtual CPS::Attribute<Bool>::Ptr importBool(UInt idx) = 0;
-		virtual CPS::Attribute<Complex>::Ptr importComplex(UInt idx) = 0;
-		virtual CPS::Attribute<Complex>::Ptr importComplexMagPhase(UInt idx) = 0;
+		private:
+			Interface& mIntf;
+		};
 
-		virtual void exportInt(CPS::Attribute<Int>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="") = 0;
-		virtual void exportReal(CPS::Attribute<Real>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="") = 0;
-		virtual void exportBool(CPS::Attribute<Bool>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="") = 0;
-		virtual void exportComplex(CPS::Attribute<Complex>::Ptr attr, UInt idx, const std::string &name="", const std::string &unit="") = 0;
+		class PostStep : public CPS::Task {
+		public:
+			explicit PostStep(Interface& intf) :
+				Task(intf.mName + ".Write"), mIntf(intf) {
+				for (const auto& [attr, _seqId] : intf.mExportAttrsDpsim) {
+					mAttributeDependencies.push_back(attr);
+				}
+				mModifiedAttributes.push_back(Scheduler::external);
+			}
 
-		/** Read data for a timestep from the interface and passes the values
-		 * to all registered current / voltage sources.
-		 */
-		virtual void readValues(bool blocking = true) = 0;
+			void execute(Real time, Int timeStepCount) override;
 
-		/** Write all exported values to the interface. Called after every timestep.
-		 * @param model Reference to the system model which should be used to
-		 * calculate needed voltages.
-		 */
-		virtual void writeValues() = 0;
+		private:
+			Interface& mIntf;
+		};
 
-		virtual CPS::Task::List getTasks() = 0;
 	};
 }
 
diff --git a/dpsim/include/dpsim/InterfaceWorker.h b/dpsim/include/dpsim/InterfaceWorker.h
new file mode 100644
index 0000000000..7b15138925
--- /dev/null
+++ b/dpsim/include/dpsim/InterfaceWorker.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include <typeinfo>
+
+#include <dpsim-models/Logger.h>
+#include <dpsim/Config.h>
+#include <dpsim/Definitions.h>
+#include <dpsim/Scheduler.h>
+#include <dpsim-models/Attribute.h>
+#include <dpsim-models/Task.h>
+#include <dpsim/Interface.h>
+
+namespace DPsim {
+
+	class InterfaceWorker {
+
+    protected:
+        bool mOpened;
+        UInt mCurrentSequenceInterfaceToDpsim = 1;
+    
+    public:
+        using Ptr = std::shared_ptr<InterfaceWorker>;
+
+        CPS::Logger::Log mLog;
+
+        InterfaceWorker() = default;
+        virtual ~InterfaceWorker() = default;
+
+        /**
+         * Function that will be called on loop in its separate thread.
+         * Should be used to read values from the environment and push them into `updatedAttrs`
+         * `updatedAttrs` will always be empty when this function is invoked
+         */
+		virtual void readValuesFromEnv(std::vector<Interface::AttributePacket>& updatedAttrs) = 0;
+
+		/**
+		 * Function that will be called on loop in its separate thread.
+         * Should be used to read values from `updatedAttrs` and write them to the environment
+         * The `updatedAttrs` list will not be cleared by the caller in between function calls
+         * When this function is called, `updatedAttrs` will include at least one value
+		 */
+        virtual void writeValuesToEnv(std::vector<Interface::AttributePacket>& updatedAttrs) = 0;
+
+        /**
+         * Open the interface and set up the connection to the environment
+         * This is guaranteed to be called before any calls to `readValuesFromEnv` and `writeValuesToEnv`
+         */
+        virtual void open() = 0;
+
+        /**
+         * Close the interface and all connections to the environment
+         * After this has been called, no further calls to `readValuesFromEnv` or `writeValuesToEnv` will occur
+         */
+        virtual void close() = 0;
+    };
+}
\ No newline at end of file
diff --git a/dpsim/include/dpsim/Simulation.h b/dpsim/include/dpsim/Simulation.h
index d01f38af19..b1613cc391 100644
--- a/dpsim/include/dpsim/Simulation.h
+++ b/dpsim/include/dpsim/Simulation.h
@@ -1,10 +1,4 @@
-/* Copyright 2017-2021 Institute for Automation of Complex Power Systems,
- *                     EONERC, RWTH Aachen University
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- *********************************************************************************/
+// SPDX-License-Identifier: Apache-2.0
 
 #pragma once
 
@@ -126,15 +120,8 @@ namespace DPsim {
 		/// Task dependencies as incoming / outgoing edges
 		Scheduler::Edges mTaskInEdges, mTaskOutEdges;
 
-		struct InterfaceMapping {
-			/// A pointer to the external interface
-			Interface *interface;
-			/// Is this interface used for synchronization of the simulation start?
-			bool syncStart;
-		};
-
 		/// Vector of Interfaces
-		std::vector<InterfaceMapping> mInterfaces;
+		std::vector<Interface::Ptr> mInterfaces;
 
 		struct LoggerMapping {
 			/// Simulation data logger
@@ -224,11 +211,9 @@ namespace DPsim {
 		/// Solve system A * x = z for x and current time
 		virtual Real step();
 		/// Synchronize simulation with remotes by exchanging intial state over interfaces
-		void sync();
+		void sync() const;
 		/// Create the schedule for the independent tasks
 		void schedule();
-		/// Reset internal state of simulation
-		void reset();
 
 		/// Schedule an event in the simulation
 		void addEvent(Event::Ptr e) {
@@ -242,15 +227,10 @@ namespace DPsim {
 		void logStepTimes(String logName);
 
 		///
-		void addInterface(Interface *eint, Bool syncStart = true) {
-			if (mInterfaces.size() > 0) {
-				mLog->warn(
-					"This simulation contains more than one interface! When using multiple InterfaceVillas instances, all of them will block the simulation thread in undefined order until the data is read / written! Continue with caution!");
-			}
-			mInterfaces.push_back({eint, syncStart});
+		void addInterface(Interface::Ptr eint) {
+			eint->setLogger(mLog);
+			mInterfaces.push_back(eint);
 		}
-		/// Return list of interfaces
-		std::vector<InterfaceMapping> & interfaces() { return mInterfaces; }
 
 #ifdef WITH_GRAPHVIZ
 		///
@@ -275,8 +255,6 @@ namespace DPsim {
 		// #### Get component attributes during simulation ####
 		CPS::AttributeBase::Ptr getIdObjAttribute(const String &comp, const String &attr);
 
-		void exportAttribute(CPS::AttributeBase::Ptr attr, Int idx, Interface* intf = nullptr);
-		void importAttribute(CPS::AttributeBase::Ptr attr, Int idx, Interface* intf = nullptr);
 		void logIdObjAttribute(const String &comp, const String &attr);
 		/// CHECK: Can we store the attribute name / UID intrinsically inside the attribute?
 		void logAttribute(String name, CPS::AttributeBase::Ptr attr);
diff --git a/dpsim/src/CMakeLists.txt b/dpsim/src/CMakeLists.txt
index b433f0f299..7d0a968325 100644
--- a/dpsim/src/CMakeLists.txt
+++ b/dpsim/src/CMakeLists.txt
@@ -15,6 +15,7 @@ set(DPSIM_SOURCES
 	ThreadLevelScheduler.cpp
 	ThreadListScheduler.cpp
 	DiakopticsSolver.cpp
+	Interface.cpp
 )
 
 set(DPSIM_INCLUDE_DIRS
@@ -23,7 +24,9 @@ set(DPSIM_INCLUDE_DIRS
 	)
 
 set(DPSIM_LIBRARIES
-	dpsim-models)
+	dpsim-models
+	readerwriterqueue
+	)
 
 if(WITH_MNASOLVERPLUGIN)
 	list(APPEND DPSIM_LIBRARIES ${CMAKE_DL_LIBS})
diff --git a/dpsim/src/Interface.cpp b/dpsim/src/Interface.cpp
new file mode 100644
index 0000000000..292e308d6d
--- /dev/null
+++ b/dpsim/src/Interface.cpp
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <dpsim/Interface.h>
+#include <dpsim/InterfaceWorker.h>
+
+using namespace CPS;
+
+namespace DPsim {
+
+    void Interface::open() {
+        mInterfaceWorker->open();
+        mOpened = true;
+
+        if (!mImportAttrsDpsim.empty()) {
+            mInterfaceReaderThread = std::thread(Interface::ReaderThread(mQueueInterfaceToDpsim, mInterfaceWorker, mOpened));
+        }
+        if (!mExportAttrsDpsim.empty()) {
+            mInterfaceWriterThread = std::thread(Interface::WriterThread(mQueueDpsimToInterface, mInterfaceWorker));
+        }
+    }
+
+    void Interface::close() {
+	    mOpened = false;
+        mQueueDpsimToInterface->emplace(AttributePacket {
+            nullptr,
+            0,
+            0,
+            AttributePacketFlags::PACKET_CLOSE_INTERFACE
+        });
+
+        if (!mExportAttrsDpsim.empty()) {
+            mInterfaceWriterThread.join();
+        }
+
+        if (!mImportAttrsDpsim.empty()) {
+            mInterfaceReaderThread.join();
+        }
+        mInterfaceWorker->close();
+    }
+
+    CPS::Task::List Interface::getTasks() {
+        auto tasks = CPS::Task::List();
+        if (!mImportAttrsDpsim.empty()) {
+            tasks.push_back(std::make_shared<Interface::PreStep>(*this));
+        }
+        if (!mExportAttrsDpsim.empty()) {
+            tasks.push_back(std::make_shared<Interface::PostStep>(*this));
+        }
+        return tasks;
+    }
+
+    void Interface::PreStep::execute(Real time, Int timeStepCount) {
+        if (timeStepCount % mIntf.mDownsampling == 0)
+            mIntf.popDpsimAttrsFromQueue();
+    }
+
+    void Interface::PostStep::execute(Real time, Int timeStepCount) {
+        if (timeStepCount % mIntf.mDownsampling == 0)
+            mIntf.pushDpsimAttrsToQueue();
+    }
+
+    void Interface::addImport(CPS::AttributeBase::Ptr attr, bool blockOnRead, bool syncOnSimulationStart) {
+        if (mOpened) {
+            mLog->error("Cannot modify interface configuration after simulation start!");
+            std::exit(1);
+        }
+
+        mImportAttrsDpsim.emplace_back(attr, 0, blockOnRead, syncOnSimulationStart);
+    }
+
+    void Interface::addExport(CPS::AttributeBase::Ptr attr) {
+        if (mOpened) {
+            mLog->error("Cannot modify interface configuration after simulation start!");
+            std::exit(1);
+        }
+
+        mExportAttrsDpsim.emplace_back(attr, 0);
+    }
+
+    void Interface::setLogger(CPS::Logger::Log log) {
+        mLog = log;
+        if (mInterfaceWorker != nullptr)
+        {
+            mInterfaceWorker->mLog = log;
+        }	
+	}
+
+    void Interface::syncImports() {
+        //Block on read until all attributes with syncOnSimulationStart are read
+        this->popDpsimAttrsFromQueue(true);
+    }
+
+    void Interface::syncExports() {
+        //Just push all the attributes
+        this->pushDpsimAttrsToQueue();
+    }
+
+    void Interface::popDpsimAttrsFromQueue(bool isSync) {
+        AttributePacket receivedPacket = {
+            nullptr,
+            0,
+            0,
+            AttributePacketFlags::PACKET_NO_FLAGS
+        };
+        UInt currentSequenceId = mNextSequenceInterfaceToDpsim;
+
+        //Wait for and dequeue all attributes that read should block on
+        //The std::find_if will look for all attributes that have not been updated in the current while loop (i. e. whose sequence ID is lower than the next expected sequence ID)
+        while (std::find_if(
+                mImportAttrsDpsim.cbegin(),
+                mImportAttrsDpsim.cend(),
+                [currentSequenceId, isSync](auto attrTuple) {
+                    auto &[_attr, seqId, blockOnRead, syncOnStart] = attrTuple;
+                    if (isSync) {
+                        return syncOnStart && seqId < currentSequenceId;
+                    } else {
+                        return blockOnRead && seqId < currentSequenceId;
+                    }
+                }) != mImportAttrsDpsim.cend()) {
+            mQueueInterfaceToDpsim->wait_dequeue(receivedPacket);
+            if (!std::get<0>(mImportAttrsDpsim[receivedPacket.attributeId])->copyValue(receivedPacket.value)) {
+                mLog->warn("Failed to copy received value onto attribute in Interface!");
+            }
+            std::get<1>(mImportAttrsDpsim[receivedPacket.attributeId]) = receivedPacket.sequenceId;
+            mNextSequenceInterfaceToDpsim = receivedPacket.sequenceId + 1;
+        }
+
+        //Fetch all remaining queue packets
+        while (mQueueInterfaceToDpsim->try_dequeue(receivedPacket)) {
+            if (!std::get<0>(mImportAttrsDpsim[receivedPacket.attributeId])->copyValue(receivedPacket.value)) {
+                mLog->warn("Failed to copy received value onto attribute in Interface!");
+            }
+            std::get<1>(mImportAttrsDpsim[receivedPacket.attributeId]) = receivedPacket.sequenceId;
+            mNextSequenceInterfaceToDpsim = receivedPacket.sequenceId + 1;
+        }
+    }
+
+    void Interface::pushDpsimAttrsToQueue() {
+        for (UInt i = 0; i < mExportAttrsDpsim.size(); i++) {
+            mQueueDpsimToInterface->emplace(AttributePacket {
+                std::get<0>(mExportAttrsDpsim[i])->cloneValueOntoNewAttribute(),
+                i,
+                std::get<1>(mExportAttrsDpsim[i]),
+                AttributePacketFlags::PACKET_NO_FLAGS
+            });
+            std::get<1>(mExportAttrsDpsim[i]) = mCurrentSequenceDpsimToInterface;
+            mCurrentSequenceDpsimToInterface++;
+        }
+    }
+
+    void Interface::WriterThread::operator() () const {
+        bool interfaceClosed = false;
+        std::vector<Interface::AttributePacket> attrsToWrite;
+        while (!interfaceClosed) {
+            AttributePacket nextPacket = {
+                nullptr,
+                0,
+                0,
+                AttributePacketFlags::PACKET_NO_FLAGS
+            };
+
+            //Wait for at least one packet
+            mQueueDpsimToInterface->wait_dequeue(nextPacket);
+            if (nextPacket.flags & AttributePacketFlags::PACKET_CLOSE_INTERFACE) {
+                interfaceClosed = true;
+            } else {
+                attrsToWrite.push_back(nextPacket);
+            }
+
+            //See if there are more packets
+            while(mQueueDpsimToInterface->try_dequeue(nextPacket)) {
+                if (nextPacket.flags & AttributePacketFlags::PACKET_CLOSE_INTERFACE) {
+                    interfaceClosed = true;
+                } else {
+                    attrsToWrite.push_back(nextPacket);
+                }
+            }
+            mInterfaceWorker->writeValuesToEnv(attrsToWrite);
+        }
+    }
+
+    void Interface::ReaderThread::operator() () const {
+        std::vector<Interface::AttributePacket>  attrsRead;
+        while (mOpened) {
+            mInterfaceWorker->readValuesFromEnv(attrsRead);
+            for (const auto &packet : attrsRead) {
+                mQueueInterfaceToDpsim->enqueue(packet);
+            }
+            attrsRead.clear();
+        }
+    }
+
+}
+
diff --git a/dpsim/src/RealTimeSimulation.cpp b/dpsim/src/RealTimeSimulation.cpp
index 9a997da439..0f4e1aea5b 100644
--- a/dpsim/src/RealTimeSimulation.cpp
+++ b/dpsim/src/RealTimeSimulation.cpp
@@ -29,14 +29,12 @@ void RealTimeSimulation::run(const Timer::StartClock::time_point &startAt) {
 	if (!mInitialized)
 		initialize();
 
-#ifdef WITH_VILLAS
 	mLog->info("Opening interfaces.");
 
-	for (auto ifm : mInterfaces)
-		ifm.interface->open(mLog);
+	for (auto intf : mInterfaces)
+		intf->open();
 
 	sync();
-#endif
 
 	auto now_time = std::chrono::system_clock::to_time_t(startAt);
 	mLog->info("Starting simulation at {} (delta_T = {} seconds)",
@@ -60,10 +58,8 @@ void RealTimeSimulation::run(const Timer::StartClock::time_point &startAt) {
 
 	mScheduler->stop();
 
-#ifdef WITH_VILLAS
-	for (auto ifm : mInterfaces)
-		ifm.interface->close();
-#endif
+	for (auto intf : mInterfaces)
+		intf->close();
 
 	for (auto lg : mLoggers)
 		lg->close();
diff --git a/dpsim/src/Simulation.cpp b/dpsim/src/Simulation.cpp
index df83bdfb90..6c58ead69e 100644
--- a/dpsim/src/Simulation.cpp
+++ b/dpsim/src/Simulation.cpp
@@ -1,10 +1,4 @@
-/* Copyright 2017-2021 Institute for Automation of Complex Power Systems,
- *                     EONERC, RWTH Aachen University
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- *********************************************************************************/
+// SPDX-License-Identifier: Apache-2.0
 
 #include <chrono>
 #include <iomanip>
@@ -172,20 +166,13 @@ void Simulation::createMNASolver() {
 	}
 }
 
-void Simulation::sync() {
-	int numOfSyncInterfaces = std::count_if(mInterfaces.begin(), mInterfaces.end(), [](InterfaceMapping ifm) {return ifm.syncStart;});
-	mLog->info("Start synchronization with remotes on {} interfaces", numOfSyncInterfaces);
+void Simulation::sync() const {
+	mLog->info("Start synchronization with remotes on interfaces");
 
-	for (auto ifm : mInterfaces) {
-		if(ifm.syncStart) {
-			// Send initial state over interface
-			ifm.interface->writeValues();
-
-			// Blocking wait for interface
-			ifm.interface->readValues(ifm.syncStart);
-
-			ifm.interface->writeValues();
-		}
+	for (auto intf : mInterfaces) {
+		intf->syncExports();
+		intf->syncImports();
+		intf->syncExports();
 	}
 
 	mLog->info("Synchronized simulation start with remotes");
@@ -201,8 +188,8 @@ void Simulation::prepSchedule() {
 		}
 	}
 
-	for (auto intfm : mInterfaces) {
-		for (auto t : intfm.interface->getTasks()) {
+	for (auto intf : mInterfaces) {
+		for (auto t : intf->getTasks()) {
 			mTasks.push_back(t);
 		}
 	}
@@ -349,8 +336,8 @@ void Simulation::start() {
 
 	mLog->info("Opening interfaces.");
 
-	for (auto ifm : mInterfaces)
-		ifm.interface->open(mLog);
+	for (auto intf : mInterfaces)
+		intf->open();
 
 	sync();
 
@@ -369,8 +356,8 @@ void Simulation::stop() {
 
 	mScheduler->stop();
 
-	for (auto ifm : mInterfaces)
-		ifm.interface->close();
+	for (auto intf : mInterfaces)
+		intf->close();
 
 	for (auto lg : mLoggers)
 		lg->close();
@@ -414,19 +401,6 @@ Real Simulation::step() {
 	return mTime;
 }
 
-/// DEPRECATED: Unused
-void Simulation::reset() {
-
-	// Resets component states
-	mSystem.reset();
-
-	for (auto l : mLoggers)
-		l->reopen();
-
-	// Force reinitialization for next run
-	mInitialized = false;
-}
-
 void Simulation::logStepTimes(String logName) {
 	auto stepTimeLog = Logger::get(logName, Logger::Level::info);
 	Logger::setLogPattern(stepTimeLog, "%v");
@@ -440,47 +414,6 @@ void Simulation::logStepTimes(String logName) {
 	mLog->info("Average step time: {:.6f}", stepTimeSum / mStepTimes.size());
 }
 
-void Simulation::exportAttribute(CPS::AttributeBase::Ptr attr, Int idx, Interface* intf) {
-	if (intf == nullptr) {
-		intf = mInterfaces[0].interface;
-	}
-	if (auto attrReal = std::dynamic_pointer_cast<CPS::Attribute<Real>>(attr.getPtr())) {
-		intf->exportReal(attrReal, idx);
-	} else if (auto attrComp = std::dynamic_pointer_cast<CPS::Attribute<Complex>>(attr.getPtr())) {
-		intf->exportComplex(attrComp, idx);
-	} else if (auto attrInt = std::dynamic_pointer_cast<CPS::Attribute<Int>>(attr.getPtr())) {
-		intf->exportInt(attrInt, idx);
-	} else if (auto attrBool = std::dynamic_pointer_cast<CPS::Attribute<Bool>>(attr.getPtr())) {
-		intf->exportBool(attrBool, idx);
-	} else {
-		mLog->error("Only scalar attributes of type Int, Bool, Real or Complex can be exported. Use the Attribute::derive methods to export individual Matrix coefficients!");
-	}
-}
-
-
-void Simulation::importAttribute(CPS::AttributeBase::Ptr attr, Int idx, Interface* intf) {
-	if (attr->isStatic()) {
-		mLog->error("Cannot import to a static attribute. Please provide a dynamic attribute!");
-		throw InvalidAttributeException();
-	}
-
-	if (intf == nullptr) {
-		intf = mInterfaces[0].interface;
-	}
-	if (auto attrReal = std::dynamic_pointer_cast<CPS::Attribute<Real>>(attr.getPtr())) {
-		attrReal->setReference(intf->importReal(idx));
-	} else if (auto attrComp = std::dynamic_pointer_cast<CPS::Attribute<Complex>>(attr.getPtr())) {
-		attrComp->setReference(intf->importComplex(idx));
-	} else if (auto attrInt = std::dynamic_pointer_cast<CPS::Attribute<Int>>(attr.getPtr())) {
-		attrInt->setReference(intf->importInt(idx));
-	} else if (auto attrBool = std::dynamic_pointer_cast<CPS::Attribute<Bool>>(attr.getPtr())) {
-		attrBool->setReference(intf->importBool(idx));
-	} else {
-		mLog->error("Only scalar attributes of type Int, Bool, Real or Complex can be imported.");
-		throw InvalidAttributeException();
-	}
-}
-
 CPS::AttributeBase::Ptr Simulation::getIdObjAttribute(const String &comp, const String &attr) {
 	IdentifiedObject::Ptr idObj = mSystem.component<IdentifiedObject>(comp);
 	if (!idObj) {
@@ -508,5 +441,6 @@ void Simulation::logIdObjAttribute(const String &comp, const String &attr) {
 }
 
 void Simulation::logAttribute(String name, CPS::AttributeBase::Ptr attr) {
+	//FIXME: Safety: This will crash when no logger is registered
 	mLoggers[0]->logAttribute(name, attr);
 }
diff --git a/dpsim/src/pybind/main.cpp b/dpsim/src/pybind/main.cpp
index c2e5ccd582..1b9510bf3c 100644
--- a/dpsim/src/pybind/main.cpp
+++ b/dpsim/src/pybind/main.cpp
@@ -78,9 +78,7 @@ PYBIND11_MODULE(dpsimpy, m) {
 		.def("start", &DPsim::Simulation::start)
 		.def("next", &DPsim::Simulation::next)
 		.def("get_idobj_attr", &DPsim::Simulation::getIdObjAttribute, "comp"_a, "attr"_a)
-		.def("add_interface", &DPsim::Simulation::addInterface, "interface"_a, "syncStart"_a = false) // cppcheck-suppress assignBoolToPointer
-		.def("export_attribute", &DPsim::Simulation::exportAttribute, "attr"_a, "idx"_a, "intf"_a = nullptr)
-		.def("import_attribute", &DPsim::Simulation::importAttribute, "attr"_a, "idx"_a, "intf"_a = nullptr)
+		.def("add_interface", &DPsim::Simulation::addInterface, "interface"_a)
 		.def("log_idobj_attribute", &DPsim::Simulation::logIdObjAttribute, "comp"_a, "attr"_a)
 		.def("log_attribute", &DPsim::Simulation::logAttribute, "name"_a, "attr"_a)
 		.def("do_init_from_nodes_and_terminals", &DPsim::Simulation::doInitFromNodesAndTerminals)
@@ -101,8 +99,7 @@ PYBIND11_MODULE(dpsimpy, m) {
 		.def("set_system", &DPsim::RealTimeSimulation::setSystem)
 		.def("run", static_cast<void (DPsim::RealTimeSimulation::*)(CPS::Int startIn)>(&DPsim::RealTimeSimulation::run))
 		.def("set_solver", &DPsim::RealTimeSimulation::setSolverType)
-		.def("set_domain", &DPsim::RealTimeSimulation::setDomain)
-		.def("add_interface", &DPsim::RealTimeSimulation::addInterface, "interface"_a, "syncStart"_a = false); // cppcheck-suppress assignBoolToPointer
+		.def("set_domain", &DPsim::RealTimeSimulation::setDomain);
 
 	py::class_<CPS::SystemTopology, std::shared_ptr<CPS::SystemTopology>>(m, "SystemTopology")
         .def(py::init<CPS::Real, CPS::TopologicalNode::List, CPS::IdentifiedObject::List>())
@@ -126,7 +123,7 @@ PYBIND11_MODULE(dpsimpy, m) {
 		.def("list_idobjects", &DPsim::SystemTopology::listIdObjects)
 		.def("init_with_powerflow", &DPsim::SystemTopology::initWithPowerflow);
 
-	py::class_<DPsim::Interface>(m, "Interface");
+	py::class_<DPsim::Interface, std::shared_ptr<DPsim::Interface>>(m, "Interface");
 
 	py::class_<DPsim::DataLogger, std::shared_ptr<DPsim::DataLogger>>(m, "Logger")
         .def(py::init<std::string>())
diff --git a/examples/villas/dpsim-file.py b/examples/villas/dpsim-file.py
index 1fcbbdea88..cf7b157d6e 100644
--- a/examples/villas/dpsim-file.py
+++ b/examples/villas/dpsim-file.py
@@ -48,11 +48,11 @@
         }
 }'''
 
-intf = dpsimpyvillas.InterfaceVillas('dpsim-file', file_config)
+intf = dpsimpyvillas.InterfaceVillas(file_config, name='dpsim-file')
+intf.export_attribute(evs.attr('i_intf').derive_coeff(0, 0), 0)
 
-sim.add_interface(intf, False)
+sim.add_interface(intf)
 #sim.import_attribute('v_intf', 'V_ref', 0)
-sim.export_attribute(evs.attr('i_intf').derive_coeff(0, 0), 0)
 
 sim.add_logger(logger)
 
diff --git a/examples/villas/dpsim-mqtt-cigre-mv-pf-profiles.py b/examples/villas/dpsim-mqtt-cigre-mv-pf-profiles.py
index e95785e85e..8b5ef2c850 100644
--- a/examples/villas/dpsim-mqtt-cigre-mv-pf-profiles.py
+++ b/examples/villas/dpsim-mqtt-cigre-mv-pf-profiles.py
@@ -79,11 +79,11 @@
 # setup exports
 for i in range(15):
     objname = 'N'+str(i)
-    sim.export_attribute(sim \
+    intf_mqtt.export_attribute(sim \
         .get_idobj_attr(objname, 'v') \
         .derive_coeff(0,0) \
         .derive_mag(), 2*i)
-    sim.export_attribute(sim \
+    intf_mqtt.export_attribute(sim \
         .get_idobj_attr(objname, 'v') \
         .derive_coeff(0,0) \
         .derive_phase(), 2*i+1)
diff --git a/examples/villas/dpsim-mqtt-import-export-MIMO.py b/examples/villas/dpsim-mqtt-import-export-MIMO.py
index 6bcf76520b..1e37e46b86 100644
--- a/examples/villas/dpsim-mqtt-import-export-MIMO.py
+++ b/examples/villas/dpsim-mqtt-import-export-MIMO.py
@@ -181,12 +181,12 @@ def dpsim():
 
     intf2 = dpsimpyvillas.InterfaceVillas(name="dpsim-shmem", config=intf_config_2)
     sim.add_interface(intf2)
-    sim.import_attribute(load.attr('P'), 0, intf=intf)
-    sim.export_attribute(n1.attr('v').derive_coeff(0,0), 0, intf=intf)
-    sim.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1, intf=intf)
-    sim.import_attribute(load.attr('Q'), 0, intf=intf2)
-    sim.export_attribute(n1.attr('v').derive_coeff(0,0), 0, intf=intf2)
-    sim.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1, intf=intf2)
+    intf.import_attribute(load.attr('P'), 0)
+    intf.export_attribute(n1.attr('v').derive_coeff(0,0), 0)
+    intf.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1)
+    intf2.import_attribute(load.attr('Q'), 0)
+    intf2.export_attribute(n1.attr('v').derive_coeff(0,0), 0)
+    intf2.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1)
   
     return sim, intf, intf2
 
diff --git a/examples/villas/dpsim-mqtt-import-export.py b/examples/villas/dpsim-mqtt-import-export.py
index e552a8f7c6..a1153dfad2 100644
--- a/examples/villas/dpsim-mqtt-import-export.py
+++ b/examples/villas/dpsim-mqtt-import-export.py
@@ -137,10 +137,10 @@ def dpsim():
     }
 
     intf = dpsimpyvillas.InterfaceVillas(name="dpsim-mqtt", config=intf_config)
+    intf.import_attribute(load.attr('P'), 0)
+    intf.export_attribute(n1.attr('v').derive_coeff(0,0), 0)
+    intf.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1)
     sim.add_interface(intf)
-    sim.import_attribute(load.attr('P'), 0)
-    sim.export_attribute(n1.attr('v').derive_coeff(0,0), 0)
-    sim.export_attribute(n2.attr('v').derive_coeff(0,0).derive_mag(), 1)
   
     return sim, intf
 
diff --git a/examples/villas/dpsim-mqtt-producer.py b/examples/villas/dpsim-mqtt-producer.py
index be7e76445e..4cd44d7a6b 100644
--- a/examples/villas/dpsim-mqtt-producer.py
+++ b/examples/villas/dpsim-mqtt-producer.py
@@ -10,7 +10,7 @@ def build_message(sequence, v_ref):
     return "[" + json.dumps(message) + "]"
 
 if __name__ == '__main__':
-    time.sleep(5)
+    time.sleep(10)
     seq = 0
     T_s = 0.01
     tf = 10.0
diff --git a/examples/villas/dpsim-mqtt.py b/examples/villas/dpsim-mqtt.py
index 47aa50be84..f50007efe2 100644
--- a/examples/villas/dpsim-mqtt.py
+++ b/examples/villas/dpsim-mqtt.py
@@ -53,11 +53,10 @@
 }'''
 
 intf = dpsimpyvillas.InterfaceVillas(name='dpsim-mqtt', config=mqtt_config)
+intf.import_attribute(evs.attr('V_ref'), 0, True)
+intf.export_attribute(r12.attr('i_intf').derive_coeff(0, 0), 0)
 
-sim.add_interface(intf, True)
-
-sim.import_attribute(evs.attr('V_ref'), 0)
-sim.export_attribute(r12.attr('i_intf').derive_coeff(0, 0), 0)
+sim.add_interface(intf)
 
 sim.add_logger(logger)
 


Test patch:
diff --git a/.github/workflows/build_test_linux_fedora.yaml b/.github/workflows/build_test_linux_fedora.yaml
index e9b23912ed..8dd639a669 100644
--- a/.github/workflows/build_test_linux_fedora.yaml
+++ b/.github/workflows/build_test_linux_fedora.yaml
@@ -14,7 +14,7 @@ jobs:
     container: sogno/dpsim:dev
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         submodules: recursive
 
@@ -34,7 +34,7 @@ jobs:
         MAKEFLAGS: "-j2"
 
     - name: Archive build directory
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         path: ${{ github.workspace }}/build
         name: build-cache
@@ -47,7 +47,7 @@ jobs:
     container: sogno/dpsim:dev
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         submodules: recursive
 
@@ -65,7 +65,7 @@ jobs:
         MAKEFLAGS: "-j2"
 
     - name: Archive build directory
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         path: ${{ github.workspace }}/build
         name: build-cache
@@ -79,7 +79,7 @@ jobs:
     container: sogno/dpsim:dev
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Restore build archive
       uses: actions/download-artifact@v3
@@ -111,7 +111,7 @@ jobs:
         pytest -v examples/Notebooks
     
     - name: Archive notebook outputs
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: pytest-output
         path: outputs/examples/Notebooks/
@@ -128,7 +128,7 @@ jobs:
        path: ${{ github.workspace }}/notebooks-new
     
     - name: Download master notebook results
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/notebooks-master
         key: notebook-output-cache-master-${{ github.sha }}
@@ -136,7 +136,7 @@ jobs:
           notebook-output-cache-master-
     
     - name: Download previous commit notebook results
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/notebooks-previous
         key: notebook-output-cache-commit-${{ github.ref }}-${{ github.sha }}
@@ -196,7 +196,7 @@ jobs:
     #needs: [linux-fedora]
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Run cppcheck
       working-directory: ${{ github.workspace }}
@@ -256,7 +256,7 @@ jobs:
       continue-on-error: true
 
     - name: Archive cppcheck output
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       if: ${{ always() && steps.cppcheck.outcome == 'failure' }}
       with:
         name: cppcheck-output
diff --git a/.github/workflows/build_test_linux_fedora_minimal.yaml b/.github/workflows/build_test_linux_fedora_minimal.yaml
index ad9ff8aa5c..343f54ca4d 100644
--- a/.github/workflows/build_test_linux_fedora_minimal.yaml
+++ b/.github/workflows/build_test_linux_fedora_minimal.yaml
@@ -14,7 +14,7 @@ jobs:
     container: sogno/dpsim:dev-minimal
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         submodules: recursive
 
@@ -34,7 +34,7 @@ jobs:
         MAKEFLAGS: "-j2"
 
     - name: Cache build directory
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/build
         key: build-cache-fedora-minimal-${{ github.sha }}
diff --git a/.github/workflows/build_test_linux_rocky.yaml b/.github/workflows/build_test_linux_rocky.yaml
index 7b6aef965a..6ea7ed6119 100644
--- a/.github/workflows/build_test_linux_rocky.yaml
+++ b/.github/workflows/build_test_linux_rocky.yaml
@@ -14,7 +14,7 @@ jobs:
     container: sogno/dpsim:dev-rocky
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         submodules: recursive
 
@@ -34,7 +34,7 @@ jobs:
         MAKEFLAGS: "-j2"
 
     - name: Cache build directory
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/build
         key: build-cache-rocky-${{ github.sha }}
@@ -45,7 +45,7 @@ jobs:
     container: sogno/dpsim:dev-rocky
     steps:
     - name: Checkout
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
       with:
         submodules: recursive
 
@@ -65,7 +65,7 @@ jobs:
         MAKEFLAGS: "-j2"
 
     - name: Cache build directory
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ github.workspace }}/build
         key: build-cache-rocky-profiling-${{ github.sha }}