diff --git a/CDB/thijs_ConfigDb.json b/CDB/thijs_ConfigDb.json
new file mode 100644
index 0000000000000000000000000000000000000000..b7e508732bf03b8919683b3cf2e34a52765a5ca3
--- /dev/null
+++ b/CDB/thijs_ConfigDb.json
@@ -0,0 +1,140 @@
+{
+    "servers": {
+        "PCC": {
+            "1": {
+                "PCC": {
+                    "LTS/PCC/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "host.docker.internal"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "SDP": {
+            "1": {
+                "SDP": {
+                    "LTS/SDP/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "dop36.astron.nl"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "example_device": {
+            "1": {
+                "example_device": {
+                    "LTS/example_device/1": {
+                         "attribute_properties": {
+                            "Ant_mask_RW": {
+                                "archive_period": [
+                                    "600000"
+                                ]
+                            }
+						},
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "host.docker.internal"
+                            ],
+                            "OPC_Server_Port": [
+                                "4842"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "ini_device": {
+            "1": {
+                "ini_device": {
+                    "LTS/ini_device/1": {
+                         "attribute_properties": {
+                            "Ant_mask_RW": {
+                                "archive_period": [
+                                    "600000"
+                                ]
+                            }
+						},
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "host.docker.internal"
+                            ],
+                            "OPC_Server_Port": [
+                                "4844"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "APSCTL": {
+            "1": {
+                "APSCTL": {
+                    "LTS/APSCTL/1": {
+                         "attribute_properties": {
+                            "Ant_mask_RW": {
+                                "archive_period": [
+                                    "600000"
+                                ]
+                            }
+						},
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "ltspi.astron.nl"
+                            ],
+                            "OPC_Server_Port": [
+                                "4844"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "SNMP": {
+            "1": {
+                "SNMP": {
+                    "LTS/SNMP/1": {
+                         "attribute_properties": {
+                            "Ant_mask_RW": {
+                                "archive_period": [
+                                    "600000"
+                                ]
+                            }
+						},
+                        "properties": {
+                            "SNMP_community": [
+                                "public"
+                            ],
+                            "SNMP_host": [
+                                "192.168.178.17"
+                            ],
+                            "SNMP_timeout": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/devices/APSCTL.py b/devices/APSCTL.py
index e4c4dd38eaa9ab3aa18665093275749a54f3d94f..f1ea50f303e608230aaeba346dbd99bde8480773 100644
--- a/devices/APSCTL.py
+++ b/devices/APSCTL.py
@@ -15,18 +15,19 @@
 from tango.server import run
 from tango.server import device_property
 from tango import AttrWriteType
-
-#attribute extention and hardware device imports
-from src.attribute_wrapper import attribute_wrapper
-from src.hardware_device import hardware_device
-import numpy
 # Additional import
 
 from clients.opcua_connection import OPCUAConnection
+from util.attribute_wrapper import attribute_wrapper
+from util.hardware_device import hardware_device
 
+from util.lofar_logging import device_logging_to_python, log_exceptions
+
+import numpy
 
 __all__ = ["APSCTL", "main"]
 
+@device_logging_to_python({"device": "APSCTL"})
 class APSCTL(hardware_device):
     """
 
@@ -68,71 +69,68 @@ class APSCTL(hardware_device):
     N_ddr = 2
     N_qsfp = 6
 
-
     # Central CP per Uniboard
-    UNB2_Power_ON_OFF_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_Power_ON_OFF_RW"], datatype=numpy.bool_, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
+    UNB2_FPGA_DDR4_SLOT_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_DDR4_SLOT_TEMP_R"], datatype=numpy.double, dims=((N_unb * N_ddr), N_fpga))
+    UNB2_I2C_bus_QSFP_STATUS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_QSFP_STATUS_R"], datatype=numpy.int64, dims=((N_unb * N_fpga), N_qsfp))
+    UNB2_I2C_bus_DDR4_STATUS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_DDR4_STATUS_R"], datatype=numpy.int64, dims=(N_ddr, N_fpga))
+    UNB2_I2C_bus_FPGA_PS_STATUS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_FPGA_PS_STATUS_R"], datatype=numpy.int64, dims=(N_unb * N_fpga,))
+    UNB2_translator_busy_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_translator_busy_R"], datatype=numpy.bool_)
+
     UNB2_Front_Panel_LED_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_Front_Panel_LED_RW"], datatype=numpy.uint8, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
-    UNB2_Mask_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_Mask_RW"], datatype=numpy.bool_, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
-    # Central MP per Uniboard
-    UNB2_I2C_bus_OK_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_OK_R"], datatype=numpy.bool_, dims=(N_unb,))
     UNB2_Front_Panel_LED_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_Front_Panel_LED_R"], datatype=numpy.uint8, dims=(N_unb,))
     UNB2_EEPROM_Serial_Number_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_EEPROM_Serial_Number_R"], datatype=numpy.str, dims=(N_unb,))
     UNB2_EEPROM_Unique_ID_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_EEPROM_Unique_ID_R"], datatype=numpy.uint32, dims=(N_unb,))
-    UNB2_DC_DC_48V_12V_VIN_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_VIN_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_DC_DC_48V_12V_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_DC_DC_48V_12V_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_DC_DC_48V_12V_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N01_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N01_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N01_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N23_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N23_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_QSFP_N23_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_FPGA_DDR4_SLOT_PART_NUMBER_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_DDR4_SLOT_PART_NUMBER_R"], datatype=numpy.str, dims=(N_unb * N_qsfp, N_fpga))
+    UNB2_monitor_rate_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_monitor_rate_RW"], datatype=numpy.double, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
+    UNB2_I2C_bus_STATUS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_STATUS_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_I2C_bus_PS_STATUS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_I2C_bus_PS_STATUS_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_mask_RW = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_mask_RW"], datatype=numpy.double, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
+    UNB2_Power_ON_OFF_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_Power_ON_OFF_R"], datatype=numpy.double, dims=(N_unb,), access=AttrWriteType.READ_WRITE)
+
+    UNB2_FPGA_QSFP_CAGE_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_TEMP_R"], datatype=numpy.double, dims=(N_unb * N_qsfp,N_fpga))
+    UNB2_FPGA_QSFP_CAGE_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_LOS_R"], datatype=numpy.uint8, dims=(N_unb * N_qsfp,N_fpga))
+    UNB2_FPGA_POL_HGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_HGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_HGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_HGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_HGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_HGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_PGM_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_PGM_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_PGM_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_PGM_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_PGM_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_PGM_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_RXGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_RXGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_RXGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_TXGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_TXGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_TXGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_TXGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_POL_FPGA_TXGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_FPGA_TXGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_POL_FPGA_CORE_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_FPGA_CORE_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_CORE_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_CORE_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_CORE_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_CORE_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_ERAM_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_ERAM_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_FPGA_POL_ERAM_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_POL_CLOCK_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_CLOCK_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_CLOCK_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_1V2_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_1V2_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_1V2_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_1V2_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_1V2_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_1V2_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_PHY_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_PHY_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_PHY_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_PHY_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
     UNB2_POL_SWITCH_PHY_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_SWITCH_PHY_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_CLOCK_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_CLOCK_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
-    UNB2_POL_CLOCK_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_CLOCK_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
-
-    # monitor points per FPGA
-    UNB2_FPGA_DDR4_SLOT_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_DDR4_SLOT_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_DDR4_SLOT_PART_NUMBER_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_DDR4_SLOT_PART_NUMBER_R"], datatype=numpy.str, dims=(N_unb * N_qsfp,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_0_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_0_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_1_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_1_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_2_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_2_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_3_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_3_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_4_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_4_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_5_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_5_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_0_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_0_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_1_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_1_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_2_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_2_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_3_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_3_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_4_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_4_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_QSFP_CAGE_5_LOS_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_QSFP_CAGE_5_LOS_R"], datatype=numpy.uint8, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_CORE_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_CORE_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_CORE_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_CORE_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_CORE_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_CORE_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_ERAM_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_ERAM_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_ERAM_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_ERAM_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_RXGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_RXGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_RXGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_RXGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_TXGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_TXGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_TXGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_TXGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_TXGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_FPGA_POL_TXGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_HGXB_VOUT_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_HGXB_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_HGXB_IOUT_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_HGXB_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_HGXB_TEMP_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_HGXB_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_PGM_VOUT_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_PGM_VOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_PGM_IOUT_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_PGM_IOUT_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
-    UNB2_FPGA_POL_PGM_TEMP_R = attribute_wrapper(comms_annotation=["2:UNB2_FPGA_POL_PGM_TEMP_R"], datatype=numpy.double, dims=(N_unb,N_fpga))
+    UNB2_POL_QSFP_N01_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_QSFP_N01_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_QSFP_N01_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N01_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_QSFP_N23_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_QSFP_N23_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_POL_QSFP_N23_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_POL_QSFP_N23_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_DC_DC_48V_12V_VIN_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_VIN_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_DC_DC_48V_12V_VOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_VOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_DC_DC_48V_12V_IOUT_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_IOUT_R"], datatype=numpy.double, dims=(N_unb,))
+    UNB2_DC_DC_48V_12V_TEMP_R = attribute_wrapper(comms_annotation=["2:PCC", "2:UNB2_DC_DC_48V_12V_TEMP_R"], datatype=numpy.double, dims=(N_unb,))
 
 
+    # QualifiedName(2: UNB2_on)
+    # QualifiedName(2: UNB2_off)
+    @log_exceptions()
     def delete_device(self):
         """Hook to delete resources allocated in init_device.
 
@@ -148,13 +146,15 @@ class APSCTL(hardware_device):
     # --------
     # overloaded functions
     # --------
-    def off(self):
+    @log_exceptions()
+    def configure_for_off(self):
         """ user code here. is called when the state is set to OFF """
 
         # Stop keep-alive
         self.opcua_connection.stop()
 
-    def initialise(self):
+    @log_exceptions()
+    def configure_for_initialise(self):
         """ user code here. is called when the sate is set to INIT """
         """Initialises the attributes and properties of the PCC."""
 
diff --git a/devices/PCC.py b/devices/PCC.py
index 266725ab410b9b56c6d326a61f22cd8e332d54d6..ff0ed0914f43272ab195b8a55f8f550e7fdd3ceb 100644
--- a/devices/PCC.py
+++ b/devices/PCC.py
@@ -117,13 +117,13 @@ class PCC(hardware_device):
     # overloaded functions
     # --------
     @log_exceptions()
-    def off(self):
+    def configure_for_off(self):
         """ user code here. is called when the state is set to OFF """
         # Stop keep-alive
         self.OPCua_client.stop()
 
     @log_exceptions()
-    def initialise(self):
+    def configure_for_initialise(self):
         """ user code here. is called when the state is set to INIT """
 
         # Init the dict that contains function to OPC-UA function mappings.
diff --git a/devices/SDP.py b/devices/SDP.py
index 6df8dec72cc803e3ecb155e54716521b30e62480..f3fbdcabacea0cd76760be08f2190f1134c76da2 100644
--- a/devices/SDP.py
+++ b/devices/SDP.py
@@ -65,39 +65,42 @@ class SDP(hardware_device):
     # Attributes
     # ----------
     # SDP will switch from fpga_mask_RW to tr_fpga_mask_RW, offer both for now as its a critical flag
-    tr_fpga_mask_RW = attribute_wrapper(comms_annotation=["1:tr_fpga_mask_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_mask_RW = attribute_wrapper(comms_annotation=["1:fpga_mask_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_scrap_R = attribute_wrapper(comms_annotation=["1:fpga_scrap_R"], datatype=numpy.int32, dims=(2048,))
-    fpga_scrap_RW = attribute_wrapper(comms_annotation=["1:fpga_scrap_RW"], datatype=numpy.int32, dims=(2048,), access=AttrWriteType.READ_WRITE)
-    fpga_status_R = attribute_wrapper(comms_annotation=["1:fpga_status_R"], datatype=numpy.bool_, dims=(16,))
-    fpga_temp_R = attribute_wrapper(comms_annotation=["1:fpga_temp_R"], datatype=numpy.float_, dims=(16,))
-    fpga_version_R = attribute_wrapper(comms_annotation=["1:fpga_version_R"], datatype=numpy.str_, dims=(16,))
-    fpga_weights_R = attribute_wrapper(comms_annotation=["1:fpga_weights_R"], datatype=numpy.int16, dims=(16, 12 * 488 * 2))
-    fpga_weights_RW = attribute_wrapper(comms_annotation=["1:fpga_weights_RW"], datatype=numpy.int16, dims=(16, 12 * 488 * 2), access=AttrWriteType.READ_WRITE)
-    fpga_processing_enable_RW = attribute_wrapper(comms_annotation=["1:fpga_processing_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_processing_enable_R = attribute_wrapper(comms_annotation=["1:fpga_processing_enable_R"], datatype=numpy.bool_, dims=(16,))
-    fpga_sst_offload_enable_RW = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sst_offload_enable_R = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_enable_R"], datatype=numpy.bool_, dims=(16,))
-    fpga_sst_offload_dest_mac_RW = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_mac_RW"], datatype=numpy.str_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sst_offload_dest_mac_R = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_mac_R"], datatype=numpy.str_, dims=(16,))
-    fpga_sst_offload_dest_ip_RW = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_ip_RW"], datatype=numpy.str_, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sst_offload_dest_ip_R = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_ip_R"], datatype=numpy.str_, dims=(16,))
-    fpga_sst_offload_dest_port_RW = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_port_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sst_offload_dest_port_R = attribute_wrapper(comms_annotation=["1:fpga_sst_offload_dest_port_R"], datatype=numpy.uint16, dims=(16,))
-    fpga_sdp_info_station_id_RW = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_station_id_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sdp_info_station_id_R = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_station_id_R"], datatype=numpy.uint16, dims=(16,)) 
-    fpga_sdp_info_observation_id_RW = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_observation_id_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sdp_info_observation_id_R = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_observation_id_R"], datatype=numpy.uint32, dims=(16,))
-    fpga_sdp_info_nyquist_sampling_zone_index_RW = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_nyquist_sampling_zone_index_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
-    fpga_sdp_info_nyquist_sampling_zone_index_R = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_nyquist_sampling_zone_index_R"], datatype=numpy.uint16, dims=(16,))
-    fpga_sdp_info_subband_calibrated_flag_R = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_subband_calibrated_flag_R"], datatype=numpy.uint16, dims=(16,)) 
-    fpga_sdp_info_beamlet_scale_R = attribute_wrapper(comms_annotation=["1:fpga_sdp_info_beamlet_scale_R"], datatype=numpy.uint16, dims=(16,)) 
-
-    tr_busy_R = attribute_wrapper(comms_annotation=["1:tr_busy_R"], datatype=numpy.bool_)
-    # NOTE: typo in node name is 'tr_reload_W' should be 'tr_reload_RW'
-    tr_reload_RW = attribute_wrapper(comms_annotation=["1:tr_reload_W"], datatype=numpy.bool_, access=AttrWriteType.READ_WRITE)
-    tr_tod_R = attribute_wrapper(comms_annotation=["1:tr_tod_R"], datatype=numpy.uint64)
-    tr_uptime_R = attribute_wrapper(comms_annotation=["1:tr_uptime_R"], datatype=numpy.uint64)
+    tr_fpga_mask_RW = attribute_wrapper(comms_annotation=["2:tr_fpga_mask_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_mask_RW = attribute_wrapper(comms_annotation=["2:fpga_mask_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_scrap_R = attribute_wrapper(comms_annotation=["2:fpga_scrap_R"], datatype=numpy.int32, dims=(2048,))
+    fpga_scrap_RW = attribute_wrapper(comms_annotation=["2:fpga_scrap_RW"], datatype=numpy.int32, dims=(2048,), access=AttrWriteType.READ_WRITE)
+    fpga_status_R = attribute_wrapper(comms_annotation=["2:fpga_status_R"], datatype=numpy.bool_, dims=(16,))
+    fpga_temp_R = attribute_wrapper(comms_annotation=["2:fpga_temp_R"], datatype=numpy.float_, dims=(16,))
+    fpga_version_R = attribute_wrapper(comms_annotation=["2:fpga_version_R"], datatype=numpy.str_, dims=(16,))
+    fpga_weights_R = attribute_wrapper(comms_annotation=["2:fpga_weights_R"], datatype=numpy.int16, dims=(16, 12 * 488 * 2))
+    fpga_weights_RW = attribute_wrapper(comms_annotation=["2:fpga_weights_RW"], datatype=numpy.int16, dims=(16, 12 * 488 * 2), access=AttrWriteType.READ_WRITE)
+    fpga_processing_enable_RW = attribute_wrapper(comms_annotation=["2:fpga_processing_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_processing_enable_R = attribute_wrapper(comms_annotation=["2:fpga_processing_enable_R"], datatype=numpy.bool_, dims=(16,))
+    fpga_sst_offload_enable_RW = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_enable_RW"], datatype=numpy.bool_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sst_offload_enable_R = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_enable_R"], datatype=numpy.bool_, dims=(16,))
+    fpga_sst_offload_dest_mac_RW = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_mac_RW"], datatype=numpy.str_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sst_offload_dest_mac_R = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_mac_R"], datatype=numpy.str_, dims=(16,))
+    fpga_sst_offload_dest_ip_RW = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_ip_RW"], datatype=numpy.str_, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sst_offload_dest_ip_R = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_ip_R"], datatype=numpy.str_, dims=(16,))
+    fpga_sst_offload_dest_port_RW = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_port_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sst_offload_dest_port_R = attribute_wrapper(comms_annotation=["2:fpga_sst_offload_dest_port_R"], datatype=numpy.uint16, dims=(16,))
+    fpga_sdp_info_station_id_RW = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_station_id_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sdp_info_station_id_R = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_station_id_R"], datatype=numpy.uint16, dims=(16,))
+    fpga_sdp_info_observation_id_RW = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_observation_id_RW"], datatype=numpy.uint32, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sdp_info_observation_id_R = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_observation_id_R"], datatype=numpy.uint32, dims=(16,))
+    fpga_sdp_info_nyquist_sampling_zone_index_RW = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_nyquist_sampling_zone_index_RW"], datatype=numpy.uint16, dims=(16,), access=AttrWriteType.READ_WRITE)
+    fpga_sdp_info_nyquist_sampling_zone_index_R = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_nyquist_sampling_zone_index_R"], datatype=numpy.uint16, dims=(16,))
+    fpga_sdp_info_subband_calibrated_flag_R = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_subband_calibrated_flag_R"], datatype=numpy.uint16, dims=(16,)) 
+    fpga_sdp_info_beamlet_scale_R = attribute_wrapper(comms_annotation=["2:fpga_sdp_info_beamlet_scale_R"], datatype=numpy.uint16, dims=(16,)) 
+
+    tr_busy_R = attribute_wrapper(comms_annotation=["2:tr_busy_R"], datatype=numpy.bool_)
+    tr_reload_RW = attribute_wrapper(comms_annotation=["2:tr_reload_RW"], datatype=numpy.bool_, access=AttrWriteType.READ_WRITE)
+    tr_tod_R = attribute_wrapper(comms_annotation=["2:tr_tod_R"], datatype=numpy.uint64)
+    tr_uptime_R = attribute_wrapper(comms_annotation=["2:tr_uptime_R"], datatype=numpy.uint64)
+
+    fpga_firmware_version_R = attribute_wrapper(comms_annotation=["2:fpga_firmware_version_R"], datatype=numpy.str_, dims=(16,))
+    fpga_hardware_version_R = attribute_wrapper(comms_annotation=["2:fpga_hardware_version_R"], datatype=numpy.str_, dims=(16,))
+    tr_software_version_R = attribute_wrapper(comms_annotation=["2:tr_software_version_R"], datatype=numpy.str_)
 
     def always_executed_hook(self):
         """Method always executed before any TANGO command is executed."""
@@ -120,14 +123,14 @@ class SDP(hardware_device):
     # overloaded functions
     # --------
     @log_exceptions()
-    def off(self):
+    def configure_for_off(self):
         """ user code here. is called when the state is set to OFF """
 
         # Stop keep-alive
         self.opcua_connection.stop()
 
     @log_exceptions()
-    def initialise(self):
+    def configure_for_initialise(self):
         """ user code here. is called when the sate is set to INIT """
         """Initialises the attributes and properties of the SDP."""
 
@@ -154,4 +157,3 @@ def main(args=None, **kwargs):
 
 if __name__ == '__main__':
     main()
-
diff --git a/devices/SNMP.py b/devices/SNMP.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b5d4a21c85cebbc562bd22baf5afec5e37b86d9
--- /dev/null
+++ b/devices/SNMP.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the PCC project
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+""" SNMP Device for LOFAR2.0
+
+"""
+
+# PyTango imports
+from tango.server import run
+from tango.server import device_property
+from tango import AttrWriteType
+
+# Additional import
+from clients.SNMP_client import SNMP_client
+from util.attribute_wrapper import attribute_wrapper
+from util.hardware_device import hardware_device
+
+import numpy
+
+__all__ = ["SNMP", "main"]
+
+
+class SNMP(hardware_device):
+    """
+
+    **Properties:**
+
+    - Device Property
+        SNMP_community
+        - Type:'DevString'
+        SNMP_host
+        - Type:'DevULong'
+        SNMP_timeout
+        - Type:'DevDouble'
+        """
+
+    # -----------------
+    # Device Properties
+    # -----------------
+
+    SNMP_community = device_property(
+        dtype='DevString',
+        mandatory=True
+    )
+
+    SNMP_host = device_property(
+        dtype='DevString',
+        mandatory=True
+    )
+
+    SNMP_timeout = device_property(
+        dtype='DevDouble',
+        mandatory=True
+    )
+
+    # ----------
+    # Attributes
+    # ----------
+
+    sys_description_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.1.0"}, datatype=numpy.str_)
+    sys_objectID_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.2.0", "type": "OID"}, datatype=numpy.str_)
+    sys_uptime_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.3.0", "type": "TimeTicks"}, datatype=numpy.int64)
+    sys_name_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.5.0"}, datatype=numpy.str_)
+    ip_route_mask_127_0_0_1_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.4.21.1.11.127.0.0.1", "type": "IpAddress"}, datatype=numpy.str_)
+    TCP_active_open_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.5.0", "type": "Counter32"}, datatype=numpy.int64)
+
+    sys_contact_RW = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str_, access=AttrWriteType.READ_WRITE)
+    sys_contact_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.1.4.0"}, datatype=numpy.str_)
+
+    TCP_Curr_estab_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.6.9.0", "type": "Gauge"}, datatype=numpy.int64)
+
+    # inferred spectrum
+    if_index_R = attribute_wrapper(comms_annotation={"oids": "1.3.6.1.2.1.2.2.1.1"}, dims=(10,), datatype=numpy.int64)
+
+
+    # --------
+    # overloaded functions
+    # --------
+    def configure_for_initialise(self):
+        """ user code here. is called when the state is set to STANDBY """
+
+        # set up the SNMP ua client
+        self.snmp_manager = SNMP_client(self.SNMP_community, self.SNMP_host, self.SNMP_timeout, self.Fault, self)
+
+        # map the attributes to the OPC ua comm client
+        for i in self.attr_list():
+            i.set_comm_client(self.snmp_manager)
+
+        self.snmp_manager.start()
+
+
+# --------
+# Commands
+# --------
+
+
+# ----------
+# Run server
+# ----------
+def main(args=None, **kwargs):
+    """Main function of the PCC module."""
+    return run((SNMP,), args=args, **kwargs)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/devices/clients/SNMP_client.py b/devices/clients/SNMP_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..6230386aa300efbf9bf23ac1491b6a10354cba2f
--- /dev/null
+++ b/devices/clients/SNMP_client.py
@@ -0,0 +1,160 @@
+from util.comms_client import CommClient
+import snmp
+import numpy
+import traceback
+
+__all__ = ["SNMP_client"]
+
+
+snmp_to_numpy_dict = {
+    snmp.types.INTEGER: numpy.int64,
+    snmp.types.TimeTicks: numpy.int64,
+    snmp.types.OCTET_STRING: numpy.str_,
+    snmp.types.OID: numpy.str_,
+    snmp.types.Counter32: numpy.int64,
+    snmp.types.Gauge32: numpy.int64,
+    snmp.types.IpAddress: numpy.str_,
+}
+
+snmp_types = {
+    "Integer": numpy.int64,
+    "Gauge": numpy.int64,
+    "TimeTick": numpy.int64,
+    "Counter32": numpy.int64,
+    "OctetString": numpy.str_,
+    "IpAddress": numpy.str_,
+    "OID": numpy.str_,
+}
+
+
+class SNMP_client(CommClient):
+    """
+        messages to keep a check on the connection. On connection failure, reconnects once.
+    """
+
+    def start(self):
+        super().start()
+
+    def __init__(self, community, host, timeout, fault_func, streams, try_interval=2):
+        """
+        Create the SNMP and connect() to it
+        """
+        super().__init__(fault_func, streams, try_interval)
+
+        self.community = community
+        self.host = host
+        self.manager = snmp.Manager(community=bytes(community, "utf8"))
+
+        # Explicitly connect
+        if not self.connect():
+            # hardware or infra is down -- needs fixing first
+            fault_func()
+            return
+
+    def connect(self):
+        """
+        Try to connect to the client
+        """
+        self.streams.debug_stream("Connecting to community: %s, host: %s", self.community, self.host)
+
+        self.connected = True
+        return True
+
+    def ping(self):
+        """
+        ping the client to make sure the connection with the client is still functional.
+        """
+        pass
+
+    def _setup_annotation(self, annotation):
+        """
+        This class's Implementation of the get_mapping function. returns the read and write functions
+        """
+
+        if isinstance(annotation, dict):
+            # check if required path inarg is present
+            if annotation.get('oids') is None:
+                ValueError("SNMP get attributes require an oid")
+            oids = annotation.get("oids")  # required
+        else:
+            TypeError("SNMP attributes require a dict with oid(s)")
+            return
+
+        dtype = annotation.get('type', None)
+
+        return oids, dtype
+
+    def setup_value_conversion(self, attribute):
+        """
+        gives the client access to the attribute_wrapper object in order to access all data it could potentially need.
+        """
+
+        dim_x = attribute.dim_x
+        dim_y = attribute.dim_y
+        dtype = attribute.numpy_type
+
+        return dim_x, dim_y, dtype
+
+    def get_oids(self, x, y, in_oid):
+
+        if x == 0:
+            x = 1
+        if y == 0:
+            y = 1
+
+        nof_oids = x * y
+
+        if nof_oids == 1:
+            # is scalar
+            if type(in_oid) is str:
+                # for ease of handling put single oid in a 1 element list
+                in_oid = [in_oid]
+            return in_oid
+
+        elif type(in_oid) is list and len(in_oid) == nof_oids:
+            # already is an array and of the right length
+            return in_oid
+        elif type(in_oid) is list and len(in_oid) != nof_oids:
+            # already is an array but the wrong length. Unable to handle this
+            raise ValueError("SNMP oids need to either be a single value or an array the size of the attribute dimensions. got: {} expected: {}x{}={}".format(len(in_oid),x,y,x*y))
+        else:
+
+            return ["{}.{}".format(in_oid, i + 1) for i in range(nof_oids)]
+
+
+    def setup_attribute(self, annotation, attribute):
+        """
+        MANDATORY function: is used by the attribute wrapper to get read/write functions. must return the read and write functions
+        """
+
+        # process the annotation
+        oids, dtype = self._setup_annotation(annotation)
+
+        # get all the necessary data to set up the read/write functions from the attribute_wrapper
+        dim_x, dim_y, numpy_type = self.setup_value_conversion(attribute)
+        oids = self.get_oids(dim_x, dim_y, oids)
+
+        def _read_function():
+            vars = self.manager.get(self.host, *oids)
+            return [snmp_to_numpy_dict[type(i.value)](str(i.value)) for i in vars]
+
+        if dtype is not None:
+            def _write_function(value):
+                if len(oids) == 1 and type(value) != list:
+                    value = [value]
+
+                for i in range(len(oids)):
+                    self.manager.set(self.host, oids[i], snmp_types[dtype](value[i]))
+        else:
+            def _write_function(value):
+                if len(oids) == 1 and type(value) != list:
+                    value = [value]
+
+                for i in range(len(oids)):
+                    self.manager.set(self.host, oids[i], value[i])
+
+
+        # return the read/write functions
+        return _read_function, _write_function
+
+
diff --git a/devices/clients/ini_client.py b/devices/clients/ini_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f4d714b57dd57d327795fe59fd6edf43eb4c9fa
--- /dev/null
+++ b/devices/clients/ini_client.py
@@ -0,0 +1,192 @@
+from util.comms_client import CommClient
+import configparser
+import numpy
+
+__all__ = ["ini_client"]
+
+
+numpy_to_ini_dict = {
+    numpy.int64: int,
+    numpy.double: float,
+    numpy.float64: float,
+    numpy.bool_: bool,
+    str: str
+}
+
+numpy_to_ini_get_dict = {
+    numpy.int64: configparser.ConfigParser.getint,
+    numpy.double: configparser.ConfigParser.getfloat,
+    numpy.float64: configparser.ConfigParser.getfloat,
+    numpy.bool_: configparser.ConfigParser.getboolean,
+    str: str
+}
+
+ini_to_numpy_dict = {
+    int: numpy.int64,
+    float: numpy.float64,
+    bool: numpy.bool_,
+    str: numpy.str_
+}
+
+import os
+
+class ini_client(CommClient):
+    """
+    this class provides an example implementation of a comms_client.
+    Durirng initialisation it creates a correctly shaped zero filled value. on read that value is returned and on write its modified.
+    """
+
+    def start(self):
+        super().start()
+
+    def __init__(self, filename, fault_func, streams, try_interval=2):
+        """
+        initialises the class and tries to connect to the client.
+        """
+        self.config = configparser.ConfigParser()
+        self.filename = filename
+
+        super().__init__(fault_func, streams, try_interval)
+
+        # Explicitly connect
+        if not self.connect():
+            # hardware or infra is down -- needs fixing first
+            fault_func()
+            return
+
+    def connect(self):
+        self.config_file = open(self.filename, "r")
+
+        self.connected = True  # set connected to true
+        return True  # if successful, return true. otherwise return false
+
+    def disconnect(self):
+        self.connected = False  # always force a reconnect, regardless of a successful disconnect
+        self.streams.debug_stream("disconnected from the 'client' ")
+
+    def _setup_annotation(self, annotation):
+        """
+        this function gives the client access to the comm client annotation data given to the attribute wrapper.
+        The annotation data can be used to provide whatever extra data is necessary in order to find/access the monitor/control point.
+
+        the annotation can be in whatever format may be required. it is up to the user to handle its content
+        example annotation may include:
+        - a file path and file line/location
+        - COM object path
+
+        Annotations:
+            name: Required, the name of the ini variable
+            section: Required,  the section of the ini variable
+
+        """
+
+        # as this is an example, just print the annotation
+        self.streams.debug_stream("annotation: {}".format(annotation))
+        name = annotation.get('name')
+        if name is None:
+            ValueError("ini client requires a variable `name` in the annotation to set/get")
+        section = annotation.get('section')
+        if section is None:
+            ValueError("requires a `section` specified in the annotation to open")
+
+        return section, name
+
+
+    def _setup_value_conversion(self, attribute):
+        """
+        gives the client access to the attribute_wrapper object in order to access all
+        necessary data such as dimensionality and data type
+        """
+
+        dim_y = attribute.dim_y
+        dim_x = attribute.dim_x
+
+        dtype = attribute.numpy_type
+
+        return dim_y, dim_x, dtype
+
+    def _setup_mapping(self, name, section, dtype, dim_y, dim_x):
+        """
+        takes all gathered data to configure and return the correct read and write functions
+        """
+
+        def read_function():
+            self.config.read_file(self.config_file)
+            value = self.config.get(section, name)
+
+            value = data_handler(value, dtype)
+
+            if dim_y > 1:
+                # if data is an image, slice it according to the y dimensions
+                value = numpy.array(numpy.split(value, indices_or_sections=dim_y))
+
+            return value
+
+        def write_function(value):
+
+            if type(value) is list:
+                write_value = ", ".join([str(v) for v in value])
+
+            else:
+                write_value = str(value)
+
+            self.config.read_file(self.config_file)
+            self.config.set(section, name, write_value)
+            fp = open(self.filename, 'w')
+            self.config.write(fp)
+
+        return read_function, write_function
+
+    def setup_attribute(self, annotation=None, attribute=None):
+        """
+        MANDATORY function: is used by the attribute wrapper to get read/write functions.
+        must return the read and write functions
+        """
+
+        # process the comms_annotation
+        section, name = self._setup_annotation(annotation)
+
+        # get all the necessary data to set up the read/write functions from the attribute_wrapper
+        dim_y, dim_x, dtype = self._setup_value_conversion(attribute)
+
+        # configure and return the read/write functions
+        read_function, write_function = self._setup_mapping(name, section, dtype, dim_y, dim_x)
+
+        # return the read/write functions
+        return read_function, write_function
+
+def data_handler(string, dtype):
+    value = []
+
+    if dtype is numpy.bool_:
+        # Handle special case for Bools
+        for i in string.split(","):
+            i = i.strip(" ")
+            if "True" == i:
+                value.append(True)
+            elif "False" == i:
+                value.append(False)
+            else:
+                raise ValueError("String to bool failed. String is not True/False, but is: '{}'".format(i))
+
+        value = dtype(value)
+
+    elif dtype is numpy.str_:
+        for i in string.split(","):
+            val = numpy.str_(i)
+            value.append(val)
+
+        value = numpy.array(value)
+
+    else:
+        # regular case, go through the separator
+        for i in string.split(","):
+            i = i.replace(" ", "")
+            val = dtype(i)
+            value.append(val)
+
+
+        # convert values from buildin type to numpy type
+        value = dtype(value)
+
+    return value
diff --git a/devices/clients/opcua_connection.py b/devices/clients/opcua_connection.py
index f55922df8dba4ca5dbb6c78db5600a7287d5f9ad..85afdfa08ef2849b8434d95bbd5c38e467a91b6c 100644
--- a/devices/clients/opcua_connection.py
+++ b/devices/clients/opcua_connection.py
@@ -51,6 +51,7 @@ class OPCUAConnection(CommClient):
             fault_func()
             return
 
+
         # determine namespace used
         try:
             if type(namespace) is str:
@@ -64,6 +65,7 @@ class OPCUAConnection(CommClient):
             self.name_space_index = 2
 
         self.obj = self.client.get_objects_node()
+        self.check_nodes()
 
     def _servername(self):
         return self.client.server_url.geturl()
@@ -83,6 +85,21 @@ class OPCUAConnection(CommClient):
             self.streams.error_stream("Could not connect to server %s: %s", self._servername(), e)
             raise Exception("Could not connect to server %s", self._servername()) from e
 
+    def check_nodes(self):
+        """
+        function purely for debugging/development only. Simply lists all top level nodes and the nodes below that
+        """
+
+        for i in self.obj.get_children():
+            print(i.get_browse_name())
+            for j in i.get_children():
+                try:
+                    print(j.get_browse_name(), j.get_data_type_as_variant_type())
+                except:
+                    print(j.get_browse_name())
+                finally:
+                    pass
+
 
     def disconnect(self):
         """
@@ -160,7 +177,6 @@ class OPCUAConnection(CommClient):
             self.streams.debug_stream("connected OPC ua node {} of type {} to attribute with dimensions: {} x {} ".format(str(node_name)[:len(node_name)-1], str(ua_type)[len("VariantType."):], dim_x, dim_y))
         except:
             pass
-
         # return the read/write functions
         return prot_attr.read_function, prot_attr.write_function
 
@@ -182,12 +198,19 @@ class ProtocolAttribute:
         """
         value = numpy.array(self.node.get_value())
 
-        if self.dim_y != 0:
+        if self.dim_y + self.dim_x == 1:
+            return numpy.array([value])
+        elif self.dim_y != 0:
             value = numpy.array(numpy.split(value, indices_or_sections=self.dim_y))
+        elif self.dim_y + self.dim_x == 1:
+            value = [numpy.array(value)]
         else:
             value = numpy.array(value)
+
         return value
 
+
+
     def write_function(self, value):
         """
         write_RW function
diff --git a/devices/examples/HW_device_template.py b/devices/examples/HW_device_template.py
index dd6cad99bd5824bd1b8a37a471aeb7796f32a05d..66b6bdb19c0b00703805dc2f76cce0d8208a36b2 100644
--- a/devices/examples/HW_device_template.py
+++ b/devices/examples/HW_device_template.py
@@ -17,6 +17,7 @@ from tango import AttrWriteType
 from util.attribute_wrapper import attribute_wrapper
 from util.hardware_device import hardware_device
 
+
 __all__ = ["HW_dev"]
 
 
@@ -55,24 +56,24 @@ class HW_dev(hardware_device):
     # --------
     # overloaded functions
     # --------
-    def fault(self):
+    def configure_for_fault(self):
         """ user code here. is called when the state is set to FAULT """
         pass
 
-    def off(self):
+    def configure_for_off(self):
         """ user code here. is called when the state is set to OFF """
         pass
 
-    def on(self):
+    def configure_for_on(self):
         """ user code here. is called when the state is set to ON """
 
         pass
 
-    def standby(self):
+    def configure_for_standby(self):
         """ user code here. is called when the state is set to STANDBY """
         pass
 
-    def initialise(self):
+    def configure_for_initialise(self):
         """ user code here. is called when the sate is set to INIT """
         pass
 
diff --git a/devices/ini_device.py b/devices/ini_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b81611830e9ed14e76141ee583a1e6a7ddb8c60
--- /dev/null
+++ b/devices/ini_device.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+#
+# This file wraps around a tango device class and provides a number of abstractions useful for hardware devices. It works together
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+"""
+
+"""
+
+# PyTango imports
+from tango.server import run
+from tango.server import device_property
+from tango import AttrWriteType
+from tango import DevState
+# Additional import
+from util.attribute_wrapper import attribute_wrapper
+from util.hardware_device import hardware_device
+
+
+import configparser
+import numpy
+
+from clients.ini_client import *
+
+
+__all__ = ["ini_device"]
+
+
+def write_ini_file(filename):
+    with open(filename, 'w') as configfile:
+
+        config = configparser.ConfigParser()
+        config['scalar'] = {}
+        config['scalar']['double_scalar_R'] = '1.2'
+        config['scalar']['bool_scalar_R'] = 'True'
+        config['scalar']['int_scalar_R'] = '5'
+        config['scalar']['str_scalar_R'] = 'this is a test'
+
+        config['spectrum'] = {}
+        config['spectrum']['double_spectrum_R'] = '1.2, 2.3, 3.4, 4.5'
+        config['spectrum']['bool_spectrum_R'] = 'True, True, False, False'
+        config['spectrum']['int_spectrum_R'] = '1, 2, 3, 4'
+        config['spectrum']['str_spectrum_R'] = '"a", "b", "c", "d"'
+
+        config['image'] = {}
+        config['image']['double_image_R'] = '1.2, 2.3, 3.4, 4.5, 5.6, 6.7'
+        config['image']['bool_image_R'] = 'True, True, False, False, True, False'
+        config['image']['int_image_R'] = '1, 2, 3, 4, 5, 6'
+        config['image']['str_image_R'] = '"a", "b", "c", "d", "e", "f"'
+
+        config.write(configfile)
+
+
+
+class ini_device(hardware_device):
+    """
+    This class is the minimal (read empty) implementation of a class using 'hardware_device'
+    """
+
+    # ----------
+    # Attributes
+    # ----------
+    """
+    attribute wrapper objects can be declared here. All attribute wrapper objects will get automatically put in a list (attr_list) for easy access
+
+    example = attribute_wrapper(comms_annotation="this is an example", datatype=numpy.double, dims=(8, 2), access=AttrWriteType.READ_WRITE)
+    ...
+
+    """
+    double_scalar_RW = attribute_wrapper(comms_annotation={"section": "scalar", "name": "double_scalar_RW"}, datatype=numpy.double, access=AttrWriteType.READ_WRITE)
+    double_scalar_R = attribute_wrapper(comms_annotation={"section": "scalar", "name": "double_scalar_R"}, datatype=numpy.double)
+    bool_scalar_RW = attribute_wrapper(comms_annotation={"section": "scalar", "name": "bool_scalar_RW"}, datatype=numpy.bool_, access=AttrWriteType.READ_WRITE)
+    bool_scalar_R = attribute_wrapper(comms_annotation={"section": "scalar", "name": "bool_scalar_R"}, datatype=numpy.bool_)
+    int_scalar_RW = attribute_wrapper(comms_annotation={"section": "scalar", "name": "int_scalar_RW"}, datatype=numpy.int64, access=AttrWriteType.READ_WRITE)
+    int_scalar_R = attribute_wrapper(comms_annotation={"section": "scalar", "name": "int_scalar_R"}, datatype=numpy.int64)
+    str_scalar_RW = attribute_wrapper(comms_annotation={"section": "scalar", "name": "str_scalar_RW"}, datatype=numpy.str_, access=AttrWriteType.READ_WRITE)
+    str_scalar_R = attribute_wrapper(comms_annotation={"section": "scalar", "name": "str_scalar_R"}, datatype=numpy.str_)
+
+    double_spectrum_RW = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "double_spectrum_RW"}, datatype=numpy.double, dims=(4,), access=AttrWriteType.READ_WRITE)
+    double_spectrum_R = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "double_spectrum_R"}, datatype=numpy.double, dims=(4,))
+    bool_spectrum_RW = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "bool_spectrum_RW"}, datatype=numpy.bool_, dims=(4,), access=AttrWriteType.READ_WRITE)
+    bool_spectrum_R = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "bool_spectrum_R"}, datatype=numpy.bool_, dims=(4,))
+    int_spectrum_RW = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "int_spectrum_RW"}, datatype=numpy.int64, dims=(4,), access=AttrWriteType.READ_WRITE)
+    int_spectrum_R = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "int_spectrum_R"}, datatype=numpy.int64, dims=(4,))
+    str_spectrum_RW = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "str_spectrum_RW"}, datatype=numpy.str_, dims=(4,), access=AttrWriteType.READ_WRITE)
+    str_spectrum_R = attribute_wrapper(comms_annotation={"section": "spectrum", "name": "str_spectrum_R"}, datatype=numpy.str_, dims=(4,))
+
+    double_image_RW = attribute_wrapper(comms_annotation={"section": "image", "name": "double_image_RW"}, datatype=numpy.double, dims=(3, 2), access=AttrWriteType.READ_WRITE)
+    double_image_R = attribute_wrapper(comms_annotation={"section": "image", "name": "double_image_R"}, datatype=numpy.double, dims=(3, 2))
+    bool_image_RW = attribute_wrapper(comms_annotation={"section": "image", "name": "bool_image_RW"}, datatype=numpy.bool_, dims=(3, 2), access=AttrWriteType.READ_WRITE)
+    bool_image_R = attribute_wrapper(comms_annotation={"section": "image", "name": "bool_image_R"}, datatype=numpy.bool_, dims=(3, 2))
+    int_image_RW = attribute_wrapper(comms_annotation={"section": "image", "name": "int_image_RW"}, datatype=numpy.int64, dims=(3, 2), access=AttrWriteType.READ_WRITE)
+    int_image_R = attribute_wrapper(comms_annotation={"section": "image", "name": "int_image_R"}, datatype=numpy.int64, dims=(3, 2))
+    str_image_RW = attribute_wrapper(comms_annotation={"section": "image", "name": "str_image_RW"}, datatype=numpy.str_, dims=(3, 2), access=AttrWriteType.READ_WRITE)
+    str_image_R = attribute_wrapper(comms_annotation={"section": "image", "name": "str_image_R"}, datatype=numpy.str_, dims=(3, 2))
+
+    # --------
+    # overloaded functions
+    # --------
+    def configure_for_initialise(self):
+        """ user code here. is called when the sate is set to INIT """
+        """Initialises the attributes and properties of the PCC."""
+
+        # set up the OPC ua client
+        self.ini_client = ini_client("example.ini", self.Fault, self)
+
+        # map an access helper class
+        for i in self.attr_list():
+            i.set_comm_client(self.ini_client)
+
+        self.ini_client.start()
+
+
+# ----------
+# Run server
+# ----------
+def main(args=None, **kwargs):
+    write_ini_file("example.ini")
+
+
+    """Main function of the hardware device module."""
+    return run((ini_device,), args=args, **kwargs)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/devices/test_device.py b/devices/test_device.py
index 5659da595706cded1e786a479a12238d723e5f1c..6a62907112ea1cf081436285aa0d21532ba24d0a 100644
--- a/devices/test_device.py
+++ b/devices/test_device.py
@@ -64,7 +64,7 @@ class test_device(hardware_device):
     # --------
     # overloaded functions
     # --------
-    def initialise(self):
+    def configure_for_initialise(self):
         """ user code here. is called when the sate is set to INIT """
         """Initialises the attributes and properties of the PCC."""
 
diff --git a/devices/util/archiver.py b/devices/util/archiver.py
old mode 100755
new mode 100644
diff --git a/devices/util/get_internal_attribute_history.py b/devices/util/get_internal_attribute_history.py
old mode 100755
new mode 100644
diff --git a/devices/util/hardware_device.py b/devices/util/hardware_device.py
index 3dea7b7c06c5fea821895f958823ff80e3aafa72..e0c9154c703a7cb82c42e9cdd7db76d68a011e05 100644
--- a/devices/util/hardware_device.py
+++ b/devices/util/hardware_device.py
@@ -85,7 +85,7 @@ class hardware_device(Device):
         self.set_state(DevState.INIT)
         self.setup_value_dict()
 
-        self.initialise()
+        self.configure_for_initialise()
 
         self.set_state(DevState.STANDBY)
 
@@ -100,7 +100,7 @@ class hardware_device(Device):
 
         :return:None
         """
-        self.on()
+        self.configure_for_on()
         self.set_state(DevState.ON)
 
     @command()
@@ -119,7 +119,7 @@ class hardware_device(Device):
         # Turn off
         self.set_state(DevState.OFF)
 
-        self.off()
+        self.configure_for_off()
 
         # Turn off again, in case of race conditions through reconnecting
         self.set_state(DevState.OFF)
@@ -138,18 +138,18 @@ class hardware_device(Device):
 
         :return:None
         """
-        self.fault()
+        self.configure_for_fault()
         self.set_state(DevState.FAULT)
 
 
     # functions that can be overloaded
-    def fault(self):
+    def configure_for_fault(self):
         pass
-    def off(self):
+    def configure_for_off(self):
         pass
-    def on(self):
+    def configure_for_on(self):
         pass
-    def initialise(self):
+    def configure_for_initialise(self):
         pass
 
     def always_executed_hook(self):
diff --git a/devices/util/lofar2_config.py b/devices/util/lofar2_config.py
old mode 100755
new mode 100644
diff --git a/devices/util/lofar_logging.py b/devices/util/lofar_logging.py
index aa7d3633138679c63fd1934cf8d5638df7b1cedf..4bedad018047614c16d65b75816ac12dc7dbd7d0 100644
--- a/devices/util/lofar_logging.py
+++ b/devices/util/lofar_logging.py
@@ -1,5 +1,6 @@
 import logging
 from functools import wraps
+import sys
 
 # Always also log the hostname because it makes the origin of the log clear.
 import socket
@@ -14,7 +15,7 @@ def configure_logger(logger: logging.Logger, log_extra=None):
         # log to the tcp_input of logstash in our ELK stack
         handler = AsynchronousLogstashHandler("elk", 5959, database_path='pending_log_messages.db')
 
-        # configure log messages 
+        # configure log messages
         formatter = LogstashFormatter(extra=log_extra, tags=["python", "lofar"])
         handler.setFormatter(formatter)
 
diff --git a/devices/util/startup.py b/devices/util/startup.py
old mode 100755
new mode 100644
index f98097f994afc340fdb168311bcb524445658f1d..0f4bcbe702b1bd1edb873234763d56455b6009b4
--- a/devices/util/startup.py
+++ b/devices/util/startup.py
@@ -34,4 +34,3 @@ def startup(device: str, force_restart: bool):
     else:
         print("Device {} has successfully reached ON state.".format(device))
     return proxy
-
diff --git a/docker-compose/Makefile b/docker-compose/Makefile
index 693bc26cbe78a45ead288c49ed2929ca7944c920..f1b2cc5788c9f75cead9c8fdfaa1793e86f2cb81 100644
--- a/docker-compose/Makefile
+++ b/docker-compose/Makefile
@@ -2,30 +2,33 @@
 MAKEPATH := $(abspath $(lastword $(MAKEFILE_LIST)))
 BASEDIR := $(notdir $(patsubst %/,%,$(dir $(MAKEPATH))))
 
+DOCKER_COMPOSE_ENV_FILE := $(abspath .env)
 COMPOSE_FILES := $(wildcard *.yml)
-COMPOSE_FILE_ARGS := $(foreach yml,$(COMPOSE_FILES),-f $(yml))
+COMPOSE_FILE_ARGS := --env-file $(DOCKER_COMPOSE_ENV_FILE) $(foreach yml,$(COMPOSE_FILES),-f $(yml))
 
 ATTACH_COMPOSE_FILE_ARGS := $(foreach yml,$(filter-out tango.yml,$(COMPOSE_FILES)),-f $(yml))
 
 # If the first make argument is "start" or "stop"...
 ifeq (start,$(firstword $(MAKECMDGOALS)))
-  SERVICE_TARGET = true
+    SERVICE_TARGET = true
 else ifeq (stop,$(firstword $(MAKECMDGOALS)))
-  SERVICE_TARGET = true
+    SERVICE_TARGET = true
 else ifeq (attach,$(firstword $(MAKECMDGOALS)))
-  SERVICE_TARGET = true
-ifndef NETWORK_MODE
-$(error NETWORK_MODE must specify the network to attach to, e.g., make NETWORK_MODE=tangonet-powersupply ...)
-endif
-ifndef TANGO_HOST
-$(error TANGO_HOST must specify the Tango database device, e.g., make TANGO_HOST=powersupply-databaseds:10000 ...)
-endif
+    SERVICE_TARGET = true
+    ifndef NETWORK_MODE
+        $(error NETWORK_MODE must specify the network to attach to, e.g., make NETWORK_MODE=tangonet-powersupply ...)
+    endif
+
+    ifndef TANGO_HOST
+        $(error TANGO_HOST must specify the Tango database device, e.g., make TANGO_HOST=powersupply-databaseds:10000 ...)
+    endif
 endif
+
 ifdef SERVICE_TARGET
-  # .. then use the rest as arguments for the make target
-  SERVICE := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
-  # ...and turn them into do-nothing targets
-  $(eval $(SERVICE):;@:)
+    # .. then use the rest as arguments for the make target
+    SERVICE := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
+    # ...and turn them into do-nothing targets
+    $(eval $(SERVICE):;@:)
 endif
 
 #
@@ -35,37 +38,41 @@ endif
 # time.
 #
 ifneq ($(CI_JOB_ID),)
-NETWORK_MODE := tangonet-$(CI_JOB_ID)
-CONTAINER_NAME_PREFIX := $(CI_JOB_ID)-
+    NETWORK_MODE := tangonet-$(CI_JOB_ID)
+    CONTAINER_NAME_PREFIX := $(CI_JOB_ID)-
 else
-CONTAINER_NAME_PREFIX :=
-$(info Network mode cannot be host for the archiver! It won't work unless you set the env var CI_JOB_ID=local)
+    CONTAINER_NAME_PREFIX :=
+    $(info Network mode cannot be host for the archiver! It won't work unless you set the env var CI_JOB_ID=local)
 endif
 
 ifeq ($(OS),Windows_NT)
     $(error Sorry, Windows is not supported yet)
 else
-	UNAME_S := $(shell uname -s)
-	ifeq ($(UNAME_S),Linux)
-		DISPLAY ?= :0.0
-		NETWORK_MODE ?= host
-		XAUTHORITY_MOUNT := /tmp/.X11-unix:/tmp/.X11-unix
-		XAUTHORITY ?= /hosthome/.Xauthority
-		# /bin/sh (=dash) does not evaluate 'docker network' conditionals correctly
-		SHELL := /bin/bash
-	endif
-	ifeq ($(UNAME_S),Darwin)
-		IF_INTERFACE := $(shell scutil --nwi | grep 'Network interfaces:' | cut -d' ' -f3)
-		IP_ADDRESS := $(shell scutil --nwi | grep 'address' | cut -d':' -f2 | tr -d ' ' | head -n1)
-		DISPLAY := $(IP_ADDRESS):0
-		# Make sure that Darwin, especially from macOS Catalina on,
-		# allows X access from our Docker containers.
-		ADD_TO_XHOST := $(shell xhost +$(IP_ADDRESS))
-		# network_mode = host doesn't work on MacOS, so fix to the internal network
-		NETWORK_MODE ?= tangonet
-		XAUTHORITY_MOUNT := $(HOME)/.Xauthority:/hosthome/.Xauthority:ro
-		XAUTHORITY := /hosthome/.Xauthority
-	endif
+    UNAME_S := $(shell uname -s)
+
+    ifeq ($(UNAME_S),Linux)
+        DISPLAY ?= :0.0
+        NETWORK_MODE ?= host
+        XAUTHORITY_MOUNT := /tmp/.X11-unix:/tmp/.X11-unix
+        XAUTHORITY ?= /hosthome/.Xauthority
+        # /bin/sh (=dash) does not evaluate 'docker network' conditionals correctly
+        SHELL := /bin/bash
+    else ifeq ($(UNAME_S),Darwin)
+        IF_INTERFACE := $(shell scutil --nwi | grep 'Network interfaces:' | cut -d' ' -f3)
+        IP_ADDRESS := $(shell scutil --nwi | grep 'address' | cut -d':' -f2 | tr -d ' ' | head -n1)
+        DISPLAY := $(IP_ADDRESS):0
+        # Make sure that Darwin, especially from macOS Catalina on,
+        # allows X access from our Docker containers.
+        ADD_TO_XHOST := $(shell xhost +$(IP_ADDRESS))
+        # network_mode = host doesn't work on MacOS, so fix to the internal network
+        ifeq ($(NETWORK_MODE),)
+            NETWORK_MODE := tangonet
+        else
+            NETWORK_MODE := $(NETWORK_MODE)
+        endif
+        XAUTHORITY_MOUNT := $(HOME)/.Xauthority:/hosthome/.Xauthority:ro
+        XAUTHORITY := /hosthome/.Xauthority
+    endif
 endif
 
 #
@@ -73,16 +80,33 @@ endif
 # machine rather than at the container.
 #
 ifeq ($(NETWORK_MODE),host)
-	TANGO_HOST := $(shell hostname):10000
-	MYSQL_HOST := $(shell hostname):3306
+    TANGO_HOST := $(shell hostname):10000
+    MYSQL_HOST := $(shell hostname):3306
 else
-	TANGO_HOST := $(CONTAINER_NAME_PREFIX)databaseds:10000
-	MYSQL_HOST := $(CONTAINER_NAME_PREFIX)tangodb:3306
+    ifeq ($(TANGO_HOST),)
+        TANGO_HOST := $(CONTAINER_NAME_PREFIX)databaseds:10000
+    else
+        TANGO_HOST := $(TANGO_HOST)
+    endif
+
+    ifeq ($(MYSQL_HOST),)
+        MYSQL_HOST := $(CONTAINER_NAME_PREFIX)tangodb:3306
+    else
+        MYSQL_HOST := $(MYSQL_HOST)
+    endif
 endif
 
-DOCKER_COMPOSE_ARGS := DISPLAY=$(DISPLAY) XAUTHORITY=$(XAUTHORITY) TANGO_HOST=$(TANGO_HOST) \
-		NETWORK_MODE=$(NETWORK_MODE) XAUTHORITY_MOUNT=$(XAUTHORITY_MOUNT) TANGO_SKA_CONTAINER_MOUNT=$(TANGO_SKA_CONTAINER_MOUNT) TANGO_LOFAR_CONTAINER_MOUNT=$(TANGO_LOFAR_CONTAINER_MOUNT) TANGO_LOFAR_CONTAINER_DIR=${TANGO_LOFAR_CONTAINER_DIR} MYSQL_HOST=$(MYSQL_HOST) \
-		CONTAINER_NAME_PREFIX=$(CONTAINER_NAME_PREFIX) COMPOSE_IGNORE_ORPHANS=true CONTAINER_EXECUTION_UID=$(shell id -u)
+DOCKER_COMPOSE_ARGS := DISPLAY=$(DISPLAY) \
+    XAUTHORITY=$(XAUTHORITY) \
+    TANGO_HOST=$(TANGO_HOST) \
+    NETWORK_MODE=$(NETWORK_MODE) \
+    XAUTHORITY_MOUNT=$(XAUTHORITY_MOUNT) \
+    TANGO_SKA_CONTAINER_MOUNT=$(TANGO_SKA_CONTAINER_MOUNT) \
+    TANGO_LOFAR_CONTAINER_MOUNT=$(TANGO_LOFAR_CONTAINER_MOUNT) \
+    TANGO_LOFAR_CONTAINER_DIR=${TANGO_LOFAR_CONTAINER_DIR} MYSQL_HOST=$(MYSQL_HOST) \
+    CONTAINER_NAME_PREFIX=$(CONTAINER_NAME_PREFIX) \
+    COMPOSE_IGNORE_ORPHANS=true \
+    CONTAINER_EXECUTION_UID=$(shell id -u)
 
 
 .PHONY: up down minimal start stop status clean pull help
@@ -132,4 +156,3 @@ clean: down  ## clear all TANGO database entries
 
 help:   ## show this help.
 	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
-
diff --git a/docker-compose/device-pcc.yml b/docker-compose/device-pcc.yml
index 73feab180ba03a8a6ae0abc11b89a460341fcf9a..026ceff8ded94fb2d6d8951e6f8b33b758dbb467 100644
--- a/docker-compose/device-pcc.yml
+++ b/docker-compose/device-pcc.yml
@@ -14,7 +14,11 @@ version: '2'
 
 services:
   device-pcc:
-    image: lofar-device-base
+    image: device-pcc
+    # build explicitly, as docker-compose does not understand a local image
+    # being shared among services.
+    build:
+        context: lofar-device-base
     container_name: ${CONTAINER_NAME_PREFIX}device-pcc
     network_mode: ${NETWORK_MODE}
     volumes:
diff --git a/docker-compose/device-sdp.yml b/docker-compose/device-sdp.yml
index fd98bfede32634c0ab380a0d2f4fe6fec0096267..30e069a5eb0d38c9ccb1e9dbe1ffaf678dd0627c 100644
--- a/docker-compose/device-sdp.yml
+++ b/docker-compose/device-sdp.yml
@@ -14,7 +14,11 @@ version: '2'
 
 services:
   device-sdp:
-    image: lofar-device-base
+    image: device-sdp
+    # build explicitly, as docker-compose does not understand a local image
+    # being shared among services.
+    build:
+        context: lofar-device-base
     container_name: ${CONTAINER_NAME_PREFIX}device-sdp
     network_mode: ${NETWORK_MODE}
     volumes:
diff --git a/docker-compose/jupyter.yml b/docker-compose/jupyter.yml
index 71daf48e08afed3ae2ba8c873b216561b9bde213..dbabfadba86064c724d9073f3077132716da5438 100644
--- a/docker-compose/jupyter.yml
+++ b/docker-compose/jupyter.yml
@@ -13,6 +13,8 @@ services:
   jupyter:
     build:
         context: jupyter
+        args:
+            CONTAINER_EXECUTION_UID: ${CONTAINER_EXECUTION_UID}
     container_name: ${CONTAINER_NAME_PREFIX}jupyter
     network_mode: ${NETWORK_MODE}
     volumes:
diff --git a/docker-compose/jupyter/Dockerfile b/docker-compose/jupyter/Dockerfile
index 97ef7ca63daa60331ae0e8dee8f5d70fa143be44..62fb7395184b9bc9f4540c8ad68acabc5dc26713 100644
--- a/docker-compose/jupyter/Dockerfile
+++ b/docker-compose/jupyter/Dockerfile
@@ -1,6 +1,10 @@
 ARG VERSION=latest
 FROM nexus.engageska-portugal.pt/ska-docker/tango-itango:${VERSION}
 
+# UID if the user that this container will run under. This is needed to give directories
+# that are needed for temporary storage the proper owner and access rights.
+ARG CONTAINER_EXECUTION_UID=1000
+
 RUN sudo pip3 install jupyter
 RUN sudo pip3 install ipykernel
 RUN sudo pip3 install jupyter_bokeh
@@ -13,7 +17,6 @@ RUN sudo jupyter nbextension enable jupyter_bokeh --py --sys-prefix
 
 # Install profiles for ipython & jupyter
 COPY ipython-profiles /opt/ipython-profiles/
-RUN sudo chown tango.tango -R /opt/ipython-profiles
 COPY jupyter-kernels /usr/local/share/jupyter/kernels/
 
 # Install patched jupyter executable
@@ -27,5 +30,7 @@ ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/
 RUN sudo chmod +x /usr/bin/tini
 
 # Make sure Jupyter can write to the home directory
-ENV HOME=/home/tango
-RUN chmod a+rwx /home/tango
+ENV HOME=/home/user
+RUN sudo mkdir -p ${HOME}
+RUN sudo chown ${CONTAINER_EXECUTION_UID} -R ${HOME}
+RUN sudo chown ${CONTAINER_EXECUTION_UID} -R /opt/ipython-profiles
diff --git a/jupyter-notebooks/ini_device.ipynb b/jupyter-notebooks/ini_device.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..ba365f263ca35e627b0430f26a02d53af059333a
--- /dev/null
+++ b/jupyter-notebooks/ini_device.ipynb
@@ -0,0 +1,238 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 128,
+   "id": "waiting-chance",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import time\n",
+    "import numpy"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 146,
+   "id": "moving-alexandria",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "d=DeviceProxy(\"LTS/ini_device/1\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 198,
+   "id": "ranking-aluminum",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Device is now in on state\n"
+     ]
+    }
+   ],
+   "source": [
+    "state = str(d.state())\n",
+    "\n",
+    "if state == \"OFF\":\n",
+    "    d.initialise()\n",
+    "    time.sleep(1)\n",
+    "state = str(d.state())\n",
+    "if state == \"STANDBY\":\n",
+    "    d.on()\n",
+    "state = str(d.state())\n",
+    "if state == \"ON\":\n",
+    "    print(\"Device is now in on state\")\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 199,
+   "id": "beneficial-evidence",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "double_scalar_RW [0.]\n",
+      "double_scalar_R [1.2]\n",
+      "bool_scalar_RW [False]\n",
+      "bool_scalar_R [ True]\n",
+      "int_scalar_RW [0]\n",
+      "int_scalar_R [5]\n",
+      "str_scalar_RW ('',)\n",
+      "str_scalar_R ('this is',)\n",
+      "double_spectrum_RW [0. 0. 0. 0.]\n",
+      "double_spectrum_R [1.2 2.3 3.4 4.5]\n",
+      "bool_spectrum_RW [False False False False]\n",
+      "bool_spectrum_R [ True  True False False]\n",
+      "int_spectrum_RW [0 0 0 0]\n",
+      "int_spectrum_R [1 2 3 4]\n",
+      "str_spectrum_RW ('', '', '', '')\n",
+      "str_spectrum_R ('\"a\"', ' \"b\"', ' \"c\"', ' \"d\"')\n",
+      "double_image_RW [[0. 0. 0.]\n",
+      " [0. 0. 0.]]\n",
+      "double_image_R [[1.2 2.3 3.4]\n",
+      " [4.5 5.6 6.7]]\n",
+      "bool_image_RW [[False False False]\n",
+      " [False False False]]\n",
+      "bool_image_R [[ True  True False]\n",
+      " [False  True False]]\n",
+      "int_image_RW [[0 0 0]\n",
+      " [0 0 0]]\n",
+      "int_image_R [[1 2 3]\n",
+      " [4 5 6]]\n",
+      "str_image_RW (('', '', ''), ('', '', ''))\n",
+      "str_image_R (('\"a\"', ' \"b\"', ' \"c\"'), (' \"d\"', ' \"e\"', ' \"f\"'))\n",
+      "State <function __get_command_func.<locals>.f at 0x7f3efee95c80>\n",
+      "Status <function __get_command_func.<locals>.f at 0x7f3efee95c80>\n"
+     ]
+    }
+   ],
+   "source": [
+    "attr_names = d.get_attribute_list()\n",
+    "\n",
+    "for i in attr_names:\n",
+    "    try:\n",
+    "        exec(\"print(i, d.{})\".format(i))\n",
+    "    except:\n",
+    "        pass\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 93,
+   "id": "sharing-mechanics",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([0])"
+      ]
+     },
+     "execution_count": 93,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "d.int_scalar_RW"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 203,
+   "id": "2f03759a",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "d.str_image_RW = [[\"1\", \"2\", \"3\"],[\"4\", \"5\", \"6\"]]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 204,
+   "id": "3187f3bb",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(('1', '2', '3'), ('4', '5', '6'))"
+      ]
+     },
+     "execution_count": 204,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "d.str_image_RW"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 192,
+   "id": "eb406dce",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "\"['a', 'b', 'c', 'd', 'e', 'f']\""
+      ]
+     },
+     "execution_count": 192,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "numpy.str_([\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 197,
+   "id": "7b270085",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "6"
+      ]
+     },
+     "execution_count": 197,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "array = []\n",
+    "string = '\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"'\n",
+    "\n",
+    "for i in string.split(\",\"):\n",
+    "    value = numpy.str_(i)\n",
+    "    array.append(value)\n",
+    "\n",
+    "len(array)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "69ecc437",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "StationControl",
+   "language": "python",
+   "name": "stationcontrol"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.7.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}